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

This commit is contained in:
dab246
2025-07-29 02:00:32 +07:00
committed by Dat H. Pham
parent 1c7778d6cc
commit b2989521d9
36 changed files with 763 additions and 270 deletions
+1
View File
@@ -106,6 +106,7 @@ export 'presentation/views/button/multi_click_widget.dart';
export 'presentation/views/semantics/checkbox_semantics.dart'; export 'presentation/views/semantics/checkbox_semantics.dart';
export 'presentation/views/semantics/text_field_semantics.dart'; export 'presentation/views/semantics/text_field_semantics.dart';
export 'presentation/views/semantics/icon_semantics.dart'; export 'presentation/views/semantics/icon_semantics.dart';
export 'presentation/views/shortcut/key_shortcut.dart';
// Resources // Resources
export 'presentation/resources/assets_paths.dart'; export 'presentation/resources/assets_paths.dart';
@@ -4,6 +4,7 @@ import 'dart:math' as math;
import 'package:core/presentation/constants/constants_ui.dart'; import 'package:core/presentation/constants/constants_ui.dart';
import 'package:core/presentation/extensions/color_extension.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/app_logger.dart';
import 'package:core/utils/html/html_interaction.dart'; import 'package:core/utils/html/html_interaction.dart';
import 'package:core/utils/html/html_template.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 OnClickHyperLinkAction = Function(Uri?);
typedef OnMailtoClicked = void Function(Uri? uri); typedef OnMailtoClicked = void Function(Uri? uri);
typedef OnIFrameKeyboardShortcutAction = void Function(KeyShortcut keyShortcut);
class HtmlContentViewerOnWeb extends StatefulWidget { class HtmlContentViewerOnWeb extends StatefulWidget {
@@ -30,6 +32,8 @@ class HtmlContentViewerOnWeb extends StatefulWidget {
final OnClickHyperLinkAction? onClickHyperLinkAction; 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 // if widthContent is bigger than width of htmlContent, set this to true let widget able to resize to width of htmlContent
final bool allowResizeToDocumentSize; final bool allowResizeToDocumentSize;
@@ -66,6 +70,7 @@ class HtmlContentViewerOnWeb extends StatefulWidget {
this.htmlContentMinWidth = ConstantsUI.htmlContentMinWidth, this.htmlContentMinWidth = ConstantsUI.htmlContentMinWidth,
this.offsetHtmlContentHeight = ConstantsUI.htmlContentOffsetHeight, this.offsetHtmlContentHeight = ConstantsUI.htmlContentOffsetHeight,
this.viewMaxHeight, this.viewMaxHeight,
this.onIFrameKeyboardShortcutAction,
}) : super(key: key); }) : super(key: key);
@override @override
@@ -116,6 +121,9 @@ class _HtmlContentViewerOnWebState extends State<HtmlContentViewerOnWeb>
} else if (_isScrollingIsAvailable && _isScrollEndEventTriggered(type)) { } else if (_isScrollingIsAvailable && _isScrollEndEventTriggered(type)) {
_handleIframeOnScrollEndListener(data, widget.scrollController!); _handleIframeOnScrollEndListener(data, widget.scrollController!);
return; return;
} else if (_isIframeKeyboardEventTriggered(type)) {
_handleOnIFrameKeyboardEvent(data);
return;
} }
if (data['message'] == iframeOnLoadMessage) { if (data['message'] == iframeOnLoadMessage) {
@@ -257,6 +265,21 @@ class _HtmlContentViewerOnWebState extends State<HtmlContentViewerOnWeb>
} }
} }
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 @override
void didUpdateWidget(covariant HtmlContentViewerOnWeb oldWidget) { void didUpdateWidget(covariant HtmlContentViewerOnWeb oldWidget) {
super.didUpdateWidget(oldWidget); super.didUpdateWidget(oldWidget);
@@ -397,6 +420,8 @@ class _HtmlContentViewerOnWebState extends State<HtmlContentViewerOnWeb>
viewId: _createdViewId, viewId: _createdViewId,
onScrollChangedEvent: onScrollChangedEvent, onScrollChangedEvent: onScrollChangedEvent,
), ),
if (widget.onIFrameKeyboardShortcutAction != null)
HtmlInteraction.scriptHandleIframeKeyboardListener(_createdViewId),
].join(); ].join();
final htmlTemplate = HtmlUtils.generateHtmlDocument( final htmlTemplate = HtmlUtils.generateHtmlDocument(
@@ -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<Object?> get props => [key, code, shift];
}
+17 -2
View File
@@ -60,7 +60,7 @@ class HtmlInteraction {
'''; ''';
static const String scriptsHandleContentSizeChanged = ''' static const String scriptsHandleContentSizeChanged = '''
<script> <script type="text/javascript">
const bodyResizeObserver = new ResizeObserver(entries => { const bodyResizeObserver = new ResizeObserver(entries => {
window.flutter_inappwebview.callHandler('$contentSizeChangedEventJSChannelName', ''); window.flutter_inappwebview.callHandler('$contentSizeChangedEventJSChannelName', '');
}) })
@@ -70,7 +70,7 @@ class HtmlInteraction {
'''; ''';
static const String scriptsHandleLazyLoadingBackgroundImage = ''' static const String scriptsHandleLazyLoadingBackgroundImage = '''
<script> <script type="text/javascript">
const lazyImages = document.querySelectorAll('[lazy]'); const lazyImages = document.querySelectorAll('[lazy]');
const lazyImageObserver = new IntersectionObserver((entries, observer) => { const lazyImageObserver = new IntersectionObserver((entries, observer) => {
entries.forEach((entry) => { entries.forEach((entry) => {
@@ -378,4 +378,19 @@ class HtmlInteraction {
</script> </script>
'''; ''';
static String scriptHandleIframeKeyboardListener(String viewId) => '''
<script type="text/javascript">
window.addEventListener('keydown', function (event) {
const payload = {
view: '$viewId',
type: 'toDart: iframeKeydown',
key: event.key,
code: event.code,
shift: event.shiftKey
};
window.parent.postMessage(JSON.stringify(payload), "*");
});
</script>
''';
} }
@@ -0,0 +1,13 @@
import 'package:flutter/services.dart';
extension LogicalKeySetHelper on Set<LogicalKeyboardKey> {
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);
}
@@ -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;
}
}
}
@@ -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;
}
}
}
@@ -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,
);
}
}
@@ -105,4 +105,14 @@ class OpenAttachmentListAction extends EmailUIAction {
isDisplayAllAttachments, isDisplayAllAttachments,
screenHeight, screenHeight,
]; ];
}
class TriggerMailViewKeyboardShortcutAction extends EmailUIAction {
TriggerMailViewKeyboardShortcutAction(this.actionType, this.email);
final EmailActionType actionType;
final PresentationEmail email;
@override
List<Object?> get props => [actionType, email];
} }
@@ -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_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/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_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/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/blob_calendar_event.dart';
import 'package:tmail_ui_user/features/email/presentation/model/composer_arguments.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, isDisplayAllAttachments: action.isDisplayAllAttachments,
); );
mailboxDashBoardController.clearEmailUIAction(); 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) { if (session != null && accountId != null) {
final moveActionRequest = await emailActionReactor.moveToMailbox( final moveActionRequest = await emailActionReactor.moveToMailbox(
session!, session!,
@@ -1267,7 +1274,7 @@ class SingleEmailController extends BaseController with AppLoaderMixin {
), ),
isSearchEmailRunning: mailboxDashBoardController.searchController.isSearchEmailRunning, isSearchEmailRunning: mailboxDashBoardController.searchController.isSearchEmailRunning,
); );
if (!context.mounted || moveActionRequest == null) return; if (moveActionRequest == null) return;
mailboxDashBoardController.moveToMailbox( mailboxDashBoardController.moveToMailbox(
session!, session!,
accountId!, accountId!,
@@ -1275,12 +1282,12 @@ class SingleEmailController extends BaseController with AppLoaderMixin {
moveActionRequest.emailIdsWithReadStatus, moveActionRequest.emailIdsWithReadStatus,
); );
if (_threadDetailController?.emailIdsPresentation.length == 1) { 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) { if (session != null && accountId != null) {
final moveActionRequest = emailActionReactor.moveToTrash( final moveActionRequest = emailActionReactor.moveToTrash(
email, email,
@@ -1289,7 +1296,7 @@ class SingleEmailController extends BaseController with AppLoaderMixin {
isSearchEmailRunning: mailboxDashBoardController.searchController.isSearchEmailRunning, isSearchEmailRunning: mailboxDashBoardController.searchController.isSearchEmailRunning,
mapDefaultMailboxIdByRole: mailboxDashBoardController.mapDefaultMailboxIdByRole, mapDefaultMailboxIdByRole: mailboxDashBoardController.mapDefaultMailboxIdByRole,
); );
if (!context.mounted || moveActionRequest == null) return; if (moveActionRequest == null) return;
mailboxDashBoardController.moveToMailbox( mailboxDashBoardController.moveToMailbox(
session!, session!,
accountId!, accountId!,
@@ -1297,12 +1304,12 @@ class SingleEmailController extends BaseController with AppLoaderMixin {
moveActionRequest.emailIdsWithReadStatus, moveActionRequest.emailIdsWithReadStatus,
); );
if (_threadDetailController?.emailIdsPresentation.length == 1) { 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) { if (session != null && accountId != null) {
final moveActionRequest = emailActionReactor.moveToSpam( final moveActionRequest = emailActionReactor.moveToSpam(
email, email,
@@ -1311,7 +1318,7 @@ class SingleEmailController extends BaseController with AppLoaderMixin {
isSearchEmailRunning: mailboxDashBoardController.searchController.isSearchEmailRunning, isSearchEmailRunning: mailboxDashBoardController.searchController.isSearchEmailRunning,
mapDefaultMailboxIdByRole: mailboxDashBoardController.mapDefaultMailboxIdByRole, mapDefaultMailboxIdByRole: mailboxDashBoardController.mapDefaultMailboxIdByRole,
); );
if (!context.mounted || moveActionRequest == null) return; if (moveActionRequest == null) return;
mailboxDashBoardController.moveToMailbox( mailboxDashBoardController.moveToMailbox(
session!, session!,
accountId!, accountId!,
@@ -1319,12 +1326,12 @@ class SingleEmailController extends BaseController with AppLoaderMixin {
moveActionRequest.emailIdsWithReadStatus, moveActionRequest.emailIdsWithReadStatus,
); );
if (_threadDetailController?.emailIdsPresentation.length == 1) { 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) { if (session != null && accountId != null) {
final moveActionRequest = emailActionReactor.unSpam( final moveActionRequest = emailActionReactor.unSpam(
email, email,
@@ -1333,7 +1340,7 @@ class SingleEmailController extends BaseController with AppLoaderMixin {
isSearchEmailRunning: mailboxDashBoardController.searchController.isSearchEmailRunning, isSearchEmailRunning: mailboxDashBoardController.searchController.isSearchEmailRunning,
mapDefaultMailboxIdByRole: mailboxDashBoardController.mapDefaultMailboxIdByRole, mapDefaultMailboxIdByRole: mailboxDashBoardController.mapDefaultMailboxIdByRole,
); );
if (!context.mounted || moveActionRequest == null) return; if (moveActionRequest == null) return;
mailboxDashBoardController.moveToMailbox( mailboxDashBoardController.moveToMailbox(
session!, session!,
accountId!, accountId!,
@@ -1341,7 +1348,7 @@ class SingleEmailController extends BaseController with AppLoaderMixin {
moveActionRequest.emailIdsWithReadStatus, moveActionRequest.emailIdsWithReadStatus,
); );
if (_threadDetailController?.emailIdsPresentation.length == 1) { if (_threadDetailController?.emailIdsPresentation.length == 1) {
_threadDetailController?.closeThreadDetailAction(context); _threadDetailController?.closeThreadDetailAction();
} }
} }
} }
@@ -1370,7 +1377,10 @@ class SingleEmailController extends BaseController with AppLoaderMixin {
toastManager.showMessageSuccess(success); toastManager.showMessageSuccess(success);
} }
void handleEmailAction(BuildContext context, PresentationEmail presentationEmail, EmailActionType actionType) { void handleEmailAction(
PresentationEmail presentationEmail,
EmailActionType actionType,
) {
switch(actionType) { switch(actionType) {
case EmailActionType.markAsUnread: case EmailActionType.markAsUnread:
if (session != null && accountId != null) { if (session != null && accountId != null) {
@@ -1403,31 +1413,31 @@ class SingleEmailController extends BaseController with AppLoaderMixin {
} }
break; break;
case EmailActionType.moveToMailbox: case EmailActionType.moveToMailbox:
moveToMailbox(context, presentationEmail); moveToMailbox(presentationEmail);
break; break;
case EmailActionType.moveToTrash: case EmailActionType.moveToTrash:
moveToTrash(context, presentationEmail); moveToTrash(presentationEmail);
break; break;
case EmailActionType.deletePermanently: case EmailActionType.deletePermanently:
deleteEmailPermanently(context, presentationEmail); deleteEmailPermanently(presentationEmail);
break; break;
case EmailActionType.moveToSpam: case EmailActionType.moveToSpam:
moveToSpam(context, presentationEmail); moveToSpam(presentationEmail);
break; break;
case EmailActionType.unSpam: case EmailActionType.unSpam:
unSpam(context, presentationEmail); unSpam(presentationEmail);
break; break;
case EmailActionType.createRule: case EmailActionType.createRule:
quickCreatingRule(context, presentationEmail.from!.first); quickCreatingRule(presentationEmail.from!.first);
break; break;
case EmailActionType.unsubscribe: case EmailActionType.unsubscribe:
_unsubscribeEmail(context, presentationEmail); _unsubscribeEmail(presentationEmail);
break; break;
case EmailActionType.archiveMessage: case EmailActionType.archiveMessage:
archiveMessage(context, presentationEmail); archiveMessage(presentationEmail);
break; break;
case EmailActionType.printAll: case EmailActionType.printAll:
_printEmail(context, presentationEmail); _printEmail(presentationEmail);
break; break;
case EmailActionType.downloadMessageAsEML: case EmailActionType.downloadMessageAsEML:
_downloadMessageAsEML(presentationEmail); _downloadMessageAsEML(presentationEmail);
@@ -1441,6 +1451,7 @@ class SingleEmailController extends BaseController with AppLoaderMixin {
case EmailActionType.replyAll: case EmailActionType.replyAll:
case EmailActionType.replyToList: case EmailActionType.replyToList:
case EmailActionType.forward: case EmailActionType.forward:
case EmailActionType.compose:
pressEmailAction(actionType, presentationEmail); pressEmailAction(actionType, presentationEmail);
break; break;
default: default:
@@ -1488,14 +1499,14 @@ class SingleEmailController extends BaseController with AppLoaderMixin {
); );
} }
void deleteEmailPermanently(BuildContext context, PresentationEmail email) { void deleteEmailPermanently(PresentationEmail email) {
emailActionReactor.deleteEmailPermanently( emailActionReactor.deleteEmailPermanently(
email, email,
onDeleteEmailRequest: (email) { onDeleteEmailRequest: (email) {
popBack(); popBack();
mailboxDashBoardController.deleteEmailPermanently(email); mailboxDashBoardController.deleteEmailPermanently(email);
if (_threadDetailController?.emailIdsPresentation.length == 1) { if (_threadDetailController?.emailIdsPresentation.length == 1) {
_threadDetailController?.closeThreadDetailAction(context); _threadDetailController?.closeThreadDetailAction();
} }
}, },
responsiveUtils: responsiveUtils, responsiveUtils: responsiveUtils,
@@ -1689,7 +1700,7 @@ class SingleEmailController extends BaseController with AppLoaderMixin {
} }
} }
void quickCreatingRule(BuildContext context, EmailAddress emailAddress) async { void quickCreatingRule(EmailAddress emailAddress) {
popBack(); popBack();
if (accountId != null && session != null) { if (accountId != null && session != null) {
@@ -1782,7 +1793,7 @@ class SingleEmailController extends BaseController with AppLoaderMixin {
threadDetailController?.isDisplayAllAttachments = false; threadDetailController?.isDisplayAllAttachments = false;
} }
void _unsubscribeEmail(BuildContext context, PresentationEmail presentationEmail) { void _unsubscribeEmail(PresentationEmail presentationEmail) {
emailActionReactor.unsubscribeEmail( emailActionReactor.unsubscribeEmail(
presentationEmail, presentationEmail,
emailUnsubscribe: emailUnsubscribe.value, 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( emailActionReactor.archiveMessage(
email, email,
onArchiveEmailRequest: (presentationEmail) { onArchiveEmailRequest: mailboxDashBoardController.archiveMessage,
mailboxDashBoardController.archiveMessage(context, presentationEmail);
},
); );
} }
void _printEmail(BuildContext context, PresentationEmail email) { void _printEmail(PresentationEmail email) {
if (_printEmailButtonState == ButtonState.disabled) { if (_printEmailButtonState == ButtonState.disabled) {
log('SingleEmailController::_printEmail: Print email started'); log('SingleEmailController::_printEmail: Print email started');
return; return;
@@ -51,6 +51,7 @@ class EmailView extends GetWidget<SingleEmailController> {
this.isFirstEmailInThreadDetail = false, this.isFirstEmailInThreadDetail = false,
this.threadSubject, this.threadSubject,
this.onToggleThreadDetailCollapseExpand, this.onToggleThreadDetailCollapseExpand,
this.onIFrameKeyboardShortcutAction,
this.scrollController, this.scrollController,
}); });
@@ -59,6 +60,7 @@ class EmailView extends GetWidget<SingleEmailController> {
final bool isFirstEmailInThreadDetail; final bool isFirstEmailInThreadDetail;
final String? threadSubject; final String? threadSubject;
final VoidCallback? onToggleThreadDetailCollapseExpand; final VoidCallback? onToggleThreadDetailCollapseExpand;
final OnIFrameKeyboardShortcutAction? onIFrameKeyboardShortcutAction;
final ScrollController? scrollController; final ScrollController? scrollController;
@override @override
@@ -83,7 +85,7 @@ class EmailView extends GetWidget<SingleEmailController> {
mailboxContain: _getMailboxContain(currentEmail), mailboxContain: _getMailboxContain(currentEmail),
isSearchActivated: controller.mailboxDashBoardController.searchController.isSearchEmailRunning, isSearchActivated: controller.mailboxDashBoardController.searchController.isSearchEmailRunning,
onBackAction: () => controller.closeEmailView(context: context), onBackAction: () => controller.closeEmailView(context: context),
onEmailActionClick: (email, action) => controller.handleEmailAction(context, email, action), onEmailActionClick: controller.handleEmailAction,
onMoreActionClick: (presentationEmail, position) { onMoreActionClick: (presentationEmail, position) {
return controller.emailActionReactor.handleMoreEmailAction( return controller.emailActionReactor.handleMoreEmailAction(
mailboxContain: controller.getMailboxContain(presentationEmail), mailboxContain: controller.getMailboxContain(presentationEmail),
@@ -92,7 +94,7 @@ class EmailView extends GetWidget<SingleEmailController> {
responsiveUtils: controller.responsiveUtils, responsiveUtils: controller.responsiveUtils,
imagePaths: controller.imagePaths, imagePaths: controller.imagePaths,
ownEmailAddress: controller.ownEmailAddress, ownEmailAddress: controller.ownEmailAddress,
handleEmailAction: (email, action) => controller.handleEmailAction(context, email, action), handleEmailAction: controller.handleEmailAction,
additionalActions: [], additionalActions: [],
emailIsRead: presentationEmail.hasRead, emailIsRead: presentationEmail.hasRead,
openBottomSheetContextMenu: controller.mailboxDashBoardController.openBottomSheetContextMenu, openBottomSheetContextMenu: controller.mailboxDashBoardController.openBottomSheetContextMenu,
@@ -256,7 +258,7 @@ class EmailView extends GetWidget<SingleEmailController> {
maxBodyHeight: bodyConstraints.maxHeight, maxBodyHeight: bodyConstraints.maxHeight,
isInsideThreadCollapsed: false, isInsideThreadCollapsed: false,
openEmailAddressDetailAction: (_, emailAddress) => controller.openEmailAddressDialog(emailAddress), openEmailAddressDetailAction: (_, emailAddress) => controller.openEmailAddressDialog(emailAddress),
onEmailActionClick: (presentationEmail, actionType) => controller.handleEmailAction(context, presentationEmail, actionType), onEmailActionClick: controller.handleEmailAction,
isInsideThreadDetailView: isInsideThreadDetailView, isInsideThreadDetailView: isInsideThreadDetailView,
emailLoaded: controller.currentEmailLoaded.value, emailLoaded: controller.currentEmailLoaded.value,
onMoreActionClick: (presentationEmail, position) => controller.emailActionReactor.handleMoreEmailAction( onMoreActionClick: (presentationEmail, position) => controller.emailActionReactor.handleMoreEmailAction(
@@ -266,7 +268,7 @@ class EmailView extends GetWidget<SingleEmailController> {
responsiveUtils: controller.responsiveUtils, responsiveUtils: controller.responsiveUtils,
imagePaths: controller.imagePaths, imagePaths: controller.imagePaths,
ownEmailAddress: controller.ownEmailAddress, ownEmailAddress: controller.ownEmailAddress,
handleEmailAction: (email, action) => controller.handleEmailAction(context, email, action), handleEmailAction: controller.handleEmailAction,
additionalActions: [ additionalActions: [
EmailActionType.reply, EmailActionType.reply,
EmailActionType.forward, EmailActionType.forward,
@@ -360,6 +362,7 @@ class EmailView extends GetWidget<SingleEmailController> {
scrollController: scrollController, scrollController: scrollController,
enableQuoteToggle: true, enableQuoteToggle: true,
fontSize: isMobileResponsive ? 16 : 14, fontSize: isMobileResponsive ? 16 : 14,
onIFrameKeyboardShortcutAction: onIFrameKeyboardShortcutAction,
), ),
); );
} else if (PlatformInfo.isIOS) { } else if (PlatformInfo.isIOS) {
@@ -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);
}
}
@@ -155,4 +155,6 @@ class SynchronizeEmailSortOrderAction extends DashBoardAction {
@override @override
List<Object?> get props => [emailSortOrderType]; List<Object?> get props => [emailSortOrderType];
} }
class ReclaimMailListKeyboardShortcutFocusAction extends DashBoardAction {}
@@ -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_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_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_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_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/handle_store_email_sort_order_extension.dart';
import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/extensions/initialize_app_language.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; int minInputLengthAutocomplete = AppConfig.defaultMinInputLengthAutocomplete;
EmailSortOrderType currentSortOrder = SearchEmailFilter.defaultSortOrder; EmailSortOrderType currentSortOrder = SearchEmailFilter.defaultSortOrder;
PaywallController? paywallController; PaywallController? paywallController;
Worker? advancedSearchVisibleWorker;
Worker? searchInputFocusWorker;
final StreamController<Either<Failure, Success>> _progressStateController = final StreamController<Either<Failure, Success>> _progressStateController =
StreamController<Either<Failure, Success>>.broadcast(); StreamController<Either<Failure, Success>>.broadcast();
@@ -376,6 +379,7 @@ class MailboxDashBoardController extends ReloadableController
if (PlatformInfo.isWeb) { if (PlatformInfo.isWeb) {
listSearchFilterScrollController = ScrollController(); listSearchFilterScrollController = ScrollController();
twakeAppManager.setExecutingBeforeReconnect(false); twakeAppManager.setExecutingBeforeReconnect(false);
registerReactiveObxVariableListener();
} }
if (PlatformInfo.isIOS) { if (PlatformInfo.isIOS) {
_registerPendingCurrentEmailIdInNotification(); _registerPendingCurrentEmailIdInNotification();
@@ -3039,18 +3043,17 @@ class MailboxDashBoardController extends ReloadableController
return false; return false;
} }
void archiveMessage(BuildContext context, PresentationEmail email) { void archiveMessage(PresentationEmail email) {
final mailboxContain = email.findMailboxContain(mapMailboxById); final mailboxContain = email.findMailboxContain(mapMailboxById);
if (mailboxContain != null) { if (mailboxContain != null) {
final archiveMailboxId = getMailboxIdByRole(PresentationMailbox.roleArchive); final archiveMailboxId = getMailboxIdByRole(PresentationMailbox.roleArchive);
final archiveMailboxPath = mapMailboxById[archiveMailboxId]?.getDisplayName(context);
if (archiveMailboxId != null) { if (archiveMailboxId != null) {
final moveToArchiveMailboxRequest = MoveToMailboxRequest( final moveToArchiveMailboxRequest = MoveToMailboxRequest(
{mailboxContain.id: [email.id!]}, {mailboxContain.id: [email.id!]},
archiveMailboxId, archiveMailboxId,
MoveAction.moving, MoveAction.moving,
EmailActionType.moveToMailbox, EmailActionType.moveToMailbox,
destinationPath: archiveMailboxPath destinationPath: getMailboxNameById(archiveMailboxId),
); );
moveToMailbox( moveToMailbox(
sessionCurrent!, 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 { void _handleRestoreDeletedMessageSuccess(EmailRecoveryActionId emailRecoveryActionId) async {
log('MailboxDashBoardController::_handleRestoreDeletedMessageSuccess():emailRecoveryActionId: $emailRecoveryActionId'); log('MailboxDashBoardController::_handleRestoreDeletedMessageSuccess():emailRecoveryActionId: $emailRecoveryActionId');
_getRestoredDeletedMessage(emailRecoveryActionId); _getRestoredDeletedMessage(emailRecoveryActionId);
@@ -3271,10 +3283,14 @@ class MailboxDashBoardController extends ReloadableController
} }
} }
bool get isEmailOpened =>
dashboardRoute.value == DashboardRoutes.threadDetailed;
@override @override
void onClose() { void onClose() {
if (PlatformInfo.isWeb) { if (PlatformInfo.isWeb) {
listSearchFilterScrollController?.dispose(); listSearchFilterScrollController?.dispose();
disposeReactiveObxVariableListener();
} }
if (PlatformInfo.isIOS) { if (PlatformInfo.isIOS) {
_iosNotificationManager?.dispose(); _iosNotificationManager?.dispose();
@@ -47,6 +47,7 @@ class SearchController extends BaseController with DateRangePickerMixin {
final listFilterOnSuggestionForm = RxList<QuickSearchFilter>(); final listFilterOnSuggestionForm = RxList<QuickSearchFilter>();
final simpleSearchIsActivated = RxBool(false); final simpleSearchIsActivated = RxBool(false);
final advancedSearchIsActivated = RxBool(false); final advancedSearchIsActivated = RxBool(false);
final isSearchInputFocused = RxBool(false);
SearchQuery? get searchQuery => searchEmailFilter.value.text; SearchQuery? get searchQuery => searchEmailFilter.value.text;
@@ -59,6 +60,17 @@ class SearchController extends BaseController with DateRangePickerMixin {
this._getAllRecentSearchLatestInteractor, this._getAllRecentSearchLatestInteractor,
); );
@override
void onInit() {
super.onInit();
searchFocus.addListener(_onSearchFocusChanged);
}
void _onSearchFocusChanged() {
log('SearchController::_onSearchFocusChanged: ${searchFocus.hasFocus}');
isSearchInputFocused.value = searchFocus.hasFocus;
}
void openAdvanceSearch() { void openAdvanceSearch() {
isAdvancedSearchViewOpen.value = true; isAdvancedSearchViewOpen.value = true;
} }
@@ -322,6 +334,7 @@ class SearchController extends BaseController with DateRangePickerMixin {
@override @override
void onClose() { void onClose() {
searchInputController.dispose(); searchInputController.dispose();
searchFocus.removeListener(_onSearchFocusChanged);
searchFocus.dispose(); searchFocus.dispose();
super.onClose(); super.onClose();
} }
@@ -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;
}
}
@@ -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/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/extensions/composer_arguments_extension.dart';
import 'package:tmail_ui_user/features/email/presentation/model/composer_arguments.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/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/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/app_routes.dart';
import 'package:tmail_ui_user/main/routes/route_navigation.dart'; import 'package:tmail_ui_user/main/routes/route_navigation.dart';
@@ -111,6 +113,9 @@ extension OpenAndCloseComposerExtension on MailboxDashBoardController {
twakeAppManager.setHasComposer(false); twakeAppManager.setHasComposer(false);
} }
dispatchThreadDetailUIAction(ReclaimMailViewKeyboardShortcutFocusAction());
dispatchAction(ReclaimMailListKeyboardShortcutFocusAction());
_handleResultAfterCloseComposer(result); _handleResultAfterCloseComposer(result);
if (composerId != null) { if (composerId != null) {
@@ -68,137 +68,141 @@ class MailboxDashBoardView extends BaseMailboxDashBoardView {
ResponsiveWidget( ResponsiveWidget(
responsiveUtils: controller.responsiveUtils, responsiveUtils: controller.responsiveUtils,
desktop: Scaffold( desktop: Scaffold(
body: GestureDetector( backgroundColor: AppColor.colorBgDesktop,
onTap: () => FocusScope.of(context).unfocus(), body: Column(
child: Container( children: [
color: AppColor.colorBgDesktop, FutureBuilder(
child: Column(children: [ future: CozyConfigManager().isInsideCozy,
FutureBuilder( builder: (context, snapshot) {
future: CozyConfigManager().isInsideCozy, if (snapshot.data == true) {
builder: (context, snapshot) { return const SizedBox.shrink();
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(() { return NavigationBarWidget(
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(
imagePaths: controller.imagePaths, imagePaths: controller.imagePaths,
onTapAction: () => accountId: accountId,
controller.openComposer(ComposerArguments()), 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( Expanded(
width: ResponsiveUtils.defaultSizeMenu, child: Column(
child: Obx(() { children: [
if (controller.searchMailboxActivated.isTrue) { const SizedBox(height: 16),
return const SearchMailboxView( FutureBuilder(
backgroundColor: AppColor.colorBgDesktop future: CozyConfigManager().isInsideCozy,
); builder: (context, snapshot) {
} else { if (snapshot.data != true) {
return MailboxView(); 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( return Padding(
padding: const EdgeInsets.only(bottom: 16), padding: const EdgeInsets.only(bottom: 16),
child: Row( child: Row(
children: [ children: [
SizedBox( SizedBox(
width: MediaQuery.sizeOf(context).width * 0.4, width: MediaQuery.sizeOf(context).width * 0.4,
height: 44, height: 44,
child: SearchInputFormWidget( child: SearchInputFormWidget(
fontSize: 15, fontSize: 15,
contentPadding: const EdgeInsets.only(bottom: 4), 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,
), ),
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,
Obx(() => RecoverDeletedMessageLoadingBannerWidget( responsiveUtils: controller.responsiveUtils,
isLoading: controller.isRecoveringDeletedMessage.value, )),
horizontalLoadingWidget: horizontalLoadingWidget, Obx(() => MarkMailboxAsReadLoadingBanner(
responsiveUtils: controller.responsiveUtils, viewState: controller.viewStateMailboxActionProgress.value,
)), )),
Obx(() => MarkMailboxAsReadLoadingBanner( Obx(() {
viewState: controller.viewStateMailboxActionProgress.value, final spamController = controller.spamReportController;
)),
Obx(() {
final spamController = controller.spamReportController;
final isSpamReportDisabled = spamController.spamReportState.value == SpamReportState.disabled; final isSpamReportDisabled = spamController.spamReportState.value == SpamReportState.disabled;
final isSpamFolderSelected = controller final isSpamFolderSelected = controller
.selectedMailbox .selectedMailbox
.value .value
?.isSpam == true; ?.isSpam == true;
final isPresentationSpamMailboxIsNull = spamController.presentationSpamMailbox.value == null; final isPresentationSpamMailboxIsNull = spamController.presentationSpamMailbox.value == null;
final isEmailOpened = controller.dashboardRoute.value == DashboardRoutes.threadDetailed; final isEmailOpened = controller.dashboardRoute.value == DashboardRoutes.threadDetailed;
@@ -209,91 +213,96 @@ class MailboxDashBoardView extends BaseMailboxDashBoardView {
return const SizedBox.shrink(); return const SizedBox.shrink();
} }
return ReportMessageBanner( return ReportMessageBanner(
imagePaths: controller.imagePaths, imagePaths: controller.imagePaths,
message: AppLocalizations message: AppLocalizations
.of(context) .of(context)
.countMessageInSpam( .countMessageInSpam(
spamController.numberOfUnreadSpamEmails, spamController.numberOfUnreadSpamEmails,
), ),
positiveName: AppLocalizations.of(context).view, positiveName: AppLocalizations.of(context).view,
isDesktop: controller isDesktop: controller
.responsiveUtils .responsiveUtils
.isDesktop(context), .isDesktop(context),
margin: SpamReportBannerWebStyles.bannerMargin, margin: SpamReportBannerWebStyles.bannerMargin,
onPositiveAction: spamController.openMailbox, onPositiveAction: spamController.openMailbox,
onNegativeAction: () => onNegativeAction: () =>
spamController.dismissSpamReportAction(context), spamController.dismissSpamReportAction(context),
); );
}), }),
QuotasBannerWidget(), QuotasBannerWidget(),
_buildVacationNotificationMessage(context), _buildVacationNotificationMessage(context),
Obx(() { Obx(() {
final selectedMailbox = controller final selectedMailbox = controller
.selectedMailbox .selectedMailbox
.value; .value;
bool showTrashBanner = controller bool showTrashBanner = controller
.isEmptyTrashBannerEnabledOnWeb( .isEmptyTrashBannerEnabledOnWeb(
context, context,
selectedMailbox, selectedMailbox,
); );
bool showSpamBanner = controller bool showSpamBanner = controller
.isEmptySpamBannerEnabledOnWeb( .isEmptySpamBannerEnabledOnWeb(
context, context,
selectedMailbox, selectedMailbox,
); );
if (showTrashBanner) { if (showTrashBanner) {
return CleanMessagesBanner( return CleanMessagesBanner(
responsiveUtils: controller.responsiveUtils, responsiveUtils: controller.responsiveUtils,
message: AppLocalizations message: AppLocalizations
.of(context) .of(context)
.message_delete_all_email_in_trash_button, .message_delete_all_email_in_trash_button,
positiveAction: AppLocalizations positiveAction: AppLocalizations
.of(context) .of(context)
.empty_trash_now, .empty_trash_now,
onPositiveAction: controller.emptyTrashAction, onPositiveAction: controller.emptyTrashAction,
margin: const EdgeInsetsDirectional.only( margin: const EdgeInsetsDirectional.only(
bottom: 8, bottom: 8,
end: 16, end: 16,
), ),
); );
} else if (showSpamBanner) { } else if (showSpamBanner) {
return CleanMessagesBanner( return CleanMessagesBanner(
responsiveUtils: controller.responsiveUtils, responsiveUtils: controller.responsiveUtils,
message: AppLocalizations message: AppLocalizations
.of(context) .of(context)
.bannerDeleteAllSpamEmailsMessage, .bannerDeleteAllSpamEmailsMessage,
positiveAction: AppLocalizations positiveAction: AppLocalizations
.of(context) .of(context)
.deleteAllSpamEmailsNow, .deleteAllSpamEmailsNow,
onPositiveAction: () => onPositiveAction: () =>
controller.openDialogEmptySpamFolder(context), controller.openDialogEmptySpamFolder(context),
margin: const EdgeInsetsDirectional.only( margin: const EdgeInsetsDirectional.only(
bottom: 8, bottom: 8,
end: 16, end: 16,
), ),
); );
} else { } else {
return const SizedBox.shrink(); return const SizedBox.shrink();
} }
}), }),
_buildListButtonQuickSearchFilter(context), _buildListButtonQuickSearchFilter(context),
Expanded(child: Obx(() { Expanded(
switch(controller.dashboardRoute.value) { child: Obx(() {
case DashboardRoutes.thread: switch(controller.dashboardRoute.value) {
return _buildThreadViewForWebDesktop(context); case DashboardRoutes.thread:
case DashboardRoutes.threadDetailed: return _buildThreadViewForWebDesktop(context);
return const ThreadDetailView(); case DashboardRoutes.threadDetailed:
default: return const ThreadDetailView();
return const SizedBox.shrink(); default:
} return const SizedBox.shrink();
})) }
])) }),
])) )
]), ],
), ),
)
],
),
)
],
), ),
), ),
tabletLarge: Obx(() { tabletLarge: Obx(() {
@@ -341,8 +341,8 @@ mixin EmailActionController {
AppUtils.launchLink(email.routeWebAsString); AppUtils.launchLink(email.routeWebAsString);
} }
void archiveMessage(BuildContext context, PresentationEmail email) { void archiveMessage(PresentationEmail email) {
mailboxDashBoardController.archiveMessage(context, email); mailboxDashBoardController.archiveMessage(email);
} }
bool hasArchiveMailbox() { bool hasArchiveMailbox() {
@@ -1198,9 +1198,7 @@ class ThreadController extends BaseController with EmailActionController {
openEmailInNewTabAction(selectedEmail); openEmailInNewTabAction(selectedEmail);
break; break;
case EmailActionType.archiveMessage: case EmailActionType.archiveMessage:
if (currentContext != null) { archiveMessage(selectedEmail);
archiveMessage(currentContext!, selectedEmail);
}
break; break;
case EmailActionType.editAsNewEmail: case EmailActionType.editAsNewEmail:
editAsNewEmail(selectedEmail); editAsNewEmail(selectedEmail);
@@ -1376,7 +1374,7 @@ class ThreadController extends BaseController with EmailActionController {
ReadActions readActions = !email.hasRead ? ReadActions.markAsRead : ReadActions.markAsUnread; ReadActions readActions = !email.hasRead ? ReadActions.markAsRead : ReadActions.markAsUnread;
markAsEmailRead(email, readActions, MarkReadAction.swipeOnThread); markAsEmailRead(email, readActions, MarkReadAction.swipeOnThread);
} else if (direction == DismissDirection.endToStart) { } else if (direction == DismissDirection.endToStart) {
archiveMessage(context, email); archiveMessage(email);
} }
return false; return false;
} }
@@ -51,4 +51,8 @@ class LoadThreadDetailAfterSelectedEmailAction extends ThreadDetailUIAction {
@override @override
List<Object?> get props => [threadId, isSentMailbox]; 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/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/mailbox_dashboard/presentation/model/dashboard_routes.dart';
import 'package:tmail_ui_user/features/thread_detail/presentation/thread_detail_controller.dart'; import 'package:tmail_ui_user/features/thread_detail/presentation/thread_detail_controller.dart';
extension CloseThreadDetailAction on ThreadDetailController { extension CloseThreadDetailAction on ThreadDetailController {
void closeThreadDetailAction(BuildContext? context) { void closeThreadDetailAction() {
if (isSearchRunning) { if (isSearchRunning) {
mailboxDashBoardController.dispatchRoute(DashboardRoutes.searchEmail); mailboxDashBoardController.dispatchRoute(DashboardRoutes.searchEmail);
} else { } else {
@@ -1,12 +1,13 @@
import 'package:core/utils/platform_info.dart'; import 'package:core/utils/platform_info.dart';
import 'package:flutter/material.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:model/extensions/presentation_email_extension.dart';
import 'package:tmail_ui_user/features/email/presentation/email_view.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/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_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_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/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/thread_detail_controller.dart';
import 'package:tmail_ui_user/features/thread_detail/presentation/widgets/thread_detail_collapsed_email.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: () { onToggleThreadDetailCollapseExpand: () {
toggleThreadDetailCollapseExpand(presentationEmail); toggleThreadDetailCollapseExpand(presentationEmail);
}, },
onIFrameKeyboardShortcutAction: keyboardShortcutFocusNode != null
? onIFrameKeyboardShortcutAction
: null,
scrollController: scrollController, scrollController: scrollController,
); );
} }
@@ -91,6 +95,9 @@ extension GetThreadDetailEmailViews on ThreadDetailController {
onToggleThreadDetailCollapseExpand: () { onToggleThreadDetailCollapseExpand: () {
toggleThreadDetailCollapseExpand(presentationEmail); toggleThreadDetailCollapseExpand(presentationEmail);
}, },
onIFrameKeyboardShortcutAction: keyboardShortcutFocusNode != null
? onIFrameKeyboardShortcutAction
: null,
scrollController: scrollController, scrollController: scrollController,
); );
}).toList(); }).toList();
@@ -0,0 +1,104 @@
import 'dart:async';
import 'package:core/presentation/views/shortcut/key_shortcut.dart';
import 'package:core/utils/app_logger.dart';
import 'package:core/utils/platform_info.dart';
import 'package:flutter/material.dart';
import 'package:model/email/presentation_email.dart';
import 'package:rxdart/rxdart.dart';
import 'package:tmail_ui_user/features/base/shortcut/app_shortcut_manager.dart';
import 'package:tmail_ui_user/features/base/shortcut/mail/mail_view_action_shortcut_type.dart';
import 'package:tmail_ui_user/features/email/presentation/action/email_ui_action.dart';
import 'package:tmail_ui_user/features/thread_detail/presentation/extension/key_shortcut_extension.dart';
import 'package:tmail_ui_user/features/thread_detail/presentation/model/mail_view_shortcut_action_view_event.dart';
import 'package:tmail_ui_user/features/thread_detail/presentation/thread_detail_controller.dart';
extension HandleMailShortcutActionsExtension on ThreadDetailController {
void onKeyDownEventAction(KeyEvent event) {
final shortcutType = AppShortcutManager.getMailViewActionFromEvent(event);
log('$runtimeType::onKeyDownEventAction:🔥Shortcut triggered: $shortcutType');
if (shortcutType == null) return;
handleMailShortcutAction(shortcutType);
}
PresentationEmail? get currentExpandedEmail {
final expandedEmailId = currentExpandedEmailId.value;
if (expandedEmailId == null) return null;
final expandedPresentationEmail = emailIdsPresentation[expandedEmailId];
if (expandedPresentationEmail == null) return null;
return expandedPresentationEmail;
}
void onKeyboardShortcutInit() {
if (PlatformInfo.isWeb) {
keyboardShortcutFocusNode = FocusNode();
keyboardShortcutFocusNode?.addListener(() {
log('$runtimeType::onInit:keyboardShortcutFocusNode: ${keyboardShortcutFocusNode?.hasFocus}');
});
shortcutActionEventController =
StreamController<MailViewShortcutActionViewEvent>();
shortcutActionEventSubscription = shortcutActionEventController
?.stream
.debounceTime(const Duration(milliseconds: 300))
.listen(_onShortcutActionViewEvent);
}
}
void _onShortcutActionViewEvent(MailViewShortcutActionViewEvent event) {
mailboxDashBoardController.dispatchEmailUIAction(
TriggerMailViewKeyboardShortcutAction(
event.actionType,
event.presentationEmail,
),
);
}
void onKeyboardShortcutDispose() {
if (PlatformInfo.isWeb) {
keyboardShortcutFocusNode?.dispose();
shortcutActionEventSubscription?.cancel();
shortcutActionEventController?.close();
}
}
void handleMailShortcutAction(MailViewActionShortcutType shortcutType) {
log('$runtimeType::handleMailShortcutAction:🔥ShortcutType: $shortcutType');
final expandedEmail = currentExpandedEmail;
if (expandedEmail == null) return;
final emailActionType = shortcutType.getEmailActionType(
currentEmail: expandedEmail,
ownerEmailAddress: mailboxDashBoardController.ownEmailAddress.value,
);
log('$runtimeType::onKeyDownEventAction:🔥EmailActionType: $emailActionType');
if (emailActionType == null) return;
shortcutActionEventController?.add(
MailViewShortcutActionViewEvent(emailActionType, expandedEmail),
);
}
void clearMailShortcutFocus() {
if (keyboardShortcutFocusNode?.hasFocus == true) {
keyboardShortcutFocusNode?.unfocus();
}
}
void refocusMailShortcutFocus() {
if (keyboardShortcutFocusNode?.hasFocus == false) {
keyboardShortcutFocusNode?.requestFocus();
}
}
void onIFrameKeyboardShortcutAction(KeyShortcut keyShortcut) {
log('$runtimeType::onIFrameKeyboardShortcutAction: ${keyShortcut.toString()}');
final shortcutType = keyShortcut.mailViewActionShortcutType;
log('$runtimeType::onIFrameKeyboardShortcutAction:🔥MailViewActionShortcutType: $shortcutType');
if (shortcutType == null) return;
handleMailShortcutAction(shortcutType);
}
}
@@ -5,7 +5,6 @@ import 'package:model/email/read_actions.dart';
import 'package:tmail_ui_user/features/thread_detail/presentation/extension/close_thread_detail_action.dart'; import 'package:tmail_ui_user/features/thread_detail/presentation/extension/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/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/thread_detail_controller.dart';
import 'package:tmail_ui_user/main/routes/route_navigation.dart';
extension HandleMarkMultipleEmailsReadSuccess on ThreadDetailController { extension HandleMarkMultipleEmailsReadSuccess on ThreadDetailController {
void handleMarkMultipleEmailsReadSuccess( void handleMarkMultipleEmailsReadSuccess(
@@ -17,7 +16,7 @@ extension HandleMarkMultipleEmailsReadSuccess on ThreadDetailController {
if (readActions == ReadActions.markAsRead) return; if (readActions == ReadActions.markAsRead) return;
if (emailsInThreadDetailInfo.length == 1) { if (emailsInThreadDetailInfo.length == 1) {
closeThreadDetailAction(currentContext); closeThreadDetailAction();
return; return;
} }
@@ -14,7 +14,6 @@ import 'package:tmail_ui_user/features/thread_detail/domain/usecases/get_thread_
import 'package:tmail_ui_user/features/thread_detail/presentation/extension/close_thread_detail_action.dart'; import 'package:tmail_ui_user/features/thread_detail/presentation/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/thread_detail_controller.dart';
import 'package:tmail_ui_user/features/thread_detail/presentation/utils/thread_detail_presentation_utils.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 { extension HandleRefreshThreadDetailAction on ThreadDetailController {
void handleRefreshThreadDetailAction( void handleRefreshThreadDetailAction(
@@ -86,7 +85,7 @@ extension HandleRefreshThreadDetailAction on ThreadDetailController {
); );
if (afterRefreshedEmailIds.isEmpty) { if (afterRefreshedEmailIds.isEmpty) {
closeThreadDetailAction(currentContext); closeThreadDetailAction();
return; return;
} }
@@ -0,0 +1,22 @@
import 'package:core/presentation/views/shortcut/key_shortcut.dart';
import 'package:tmail_ui_user/features/base/shortcut/mail/mail_view_action_shortcut_type.dart';
extension KeyShortcutExtension on KeyShortcut {
MailViewActionShortcutType? get mailViewActionShortcutType {
if (matches('r')) {
return MailViewActionShortcutType.reply;
} else if (matches('r', shift: true)) {
return MailViewActionShortcutType.replyAll;
} else if (matches('f')) {
return MailViewActionShortcutType.forward;
} else if (matches('u')) {
return MailViewActionShortcutType.markAsUnread;
} else if (matches('n')) {
return MailViewActionShortcutType.newMessage;
} else if (matches('backspace') || matches('delete')) {
return MailViewActionShortcutType.delete;
}
return null;
}
}
@@ -5,7 +5,6 @@ import 'package:tmail_ui_user/features/email/domain/state/mark_as_email_read_sta
import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/extensions/update_current_emails_flags_extension.dart'; import 'package:tmail_ui_user/features/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/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/thread_detail_controller.dart';
import 'package:tmail_ui_user/main/routes/route_navigation.dart';
extension MarkCollapsedEmailReadSuccess on ThreadDetailController { extension MarkCollapsedEmailReadSuccess on ThreadDetailController {
void markCollapsedEmailReadSuccess(MarkAsEmailReadSuccess success) { void markCollapsedEmailReadSuccess(MarkAsEmailReadSuccess success) {
@@ -41,7 +40,7 @@ extension MarkCollapsedEmailReadSuccess on ThreadDetailController {
) { ) {
if (success.readActions == ReadActions.markAsRead) return; if (success.readActions == ReadActions.markAsRead) return;
if (emailIdsPresentation.length == 1) { if (emailIdsPresentation.length == 1) {
closeThreadDetailAction(currentContext); closeThreadDetailAction();
} else { } else {
emailIdsPresentation[success.emailId] = emailIdsPresentation emailIdsPresentation[success.emailId] = emailIdsPresentation
[success.emailId] [success.emailId]
@@ -72,7 +72,7 @@ extension OnThreadDetailActionClick on ThreadDetailController {
case EmailActionType.deletePermanently: case EmailActionType.deletePermanently:
mailboxDashBoardController.permanentDeleteMultipleEmailInThreadDetail( mailboxDashBoardController.permanentDeleteMultipleEmailInThreadDetail(
emailsInThreadDetailInfo, emailsInThreadDetailInfo,
onConfirm: () => closeThreadDetailAction(currentContext), onConfirm: closeThreadDetailAction,
); );
break; break;
case EmailActionType.moveToMailbox: case EmailActionType.moveToMailbox:
@@ -183,7 +183,7 @@ extension OnThreadDetailActionClick on ThreadDetailController {
} }
void _moveToMailbox(MailboxId mailboxId, EmailActionType emailActionType) { void _moveToMailbox(MailboxId mailboxId, EmailActionType emailActionType) {
closeThreadDetailAction(currentContext); closeThreadDetailAction();
mailboxDashBoardController.moveMultipleEmailInThreadDetail( mailboxDashBoardController.moveMultipleEmailInThreadDetail(
emailsInThreadDetailInfo, emailsInThreadDetailInfo,
destinationMailboxId: mailboxId, destinationMailboxId: mailboxId,
@@ -20,7 +20,7 @@ extension ThreadDetailOnSelectedEmailUpdated on ThreadDetailController {
BuildContext? context, BuildContext? context,
) { ) {
if (selectedEmail?.id == null) { if (selectedEmail?.id == null) {
closeThreadDetailAction(context); closeThreadDetailAction();
return; return;
} }
@@ -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_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_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_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/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/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/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/features/thread_detail/presentation/thread_detail_manager.dart';
import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; import 'package:tmail_ui_user/main/localizations/app_localizations.dart';
import 'package:tmail_ui_user/main/routes/route_navigation.dart'; import 'package:tmail_ui_user/main/routes/route_navigation.dart';
@@ -124,6 +126,9 @@ class ThreadDetailController extends BaseController {
CreateNewEmailRuleFilterInteractor? _createNewEmailRuleFilterInteractor; CreateNewEmailRuleFilterInteractor? _createNewEmailRuleFilterInteractor;
bool loadThreadOnThreadChanged = false; bool loadThreadOnThreadChanged = false;
bool isDisplayAllAttachments = false; bool isDisplayAllAttachments = false;
FocusNode? keyboardShortcutFocusNode;
StreamController<MailViewShortcutActionViewEvent>? shortcutActionEventController;
StreamSubscription<MailViewShortcutActionViewEvent>? shortcutActionEventSubscription;
AccountId? get accountId => mailboxDashBoardController.accountId.value; AccountId? get accountId => mailboxDashBoardController.accountId.value;
Session? get session => mailboxDashBoardController.sessionCurrent; Session? get session => mailboxDashBoardController.sessionCurrent;
@@ -197,6 +202,10 @@ class ThreadDetailController extends BaseController {
threadId: action.threadId, threadId: action.threadId,
isSentMailbox: action.isSentMailbox, isSentMailbox: action.isSentMailbox,
); );
} else if (action is ReclaimMailViewKeyboardShortcutFocusAction) {
refocusMailShortcutFocus();
} else if (action is ClearMailViewKeyboardShortcutFocusAction) {
clearMailShortcutFocus();
} }
// Reset [threadDetailUIAction] to original value // Reset [threadDetailUIAction] to original value
mailboxDashBoardController.dispatchThreadDetailUIAction( mailboxDashBoardController.dispatchThreadDetailUIAction(
@@ -212,6 +221,8 @@ class ThreadDetailController extends BaseController {
); );
} }
}); });
onKeyboardShortcutInit();
} }
bool _validateLoadThread(ThreadId? threadId) { bool _validateLoadThread(ThreadId? threadId) {
@@ -295,4 +306,10 @@ class ThreadDetailController extends BaseController {
} }
super.handleFailureViewState(failure); 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:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:model/mailbox/presentation_mailbox.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/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/styles/email_view_app_bar_widget_styles.dart';
import 'package:tmail_ui_user/features/email/presentation/widgets/email_view_bottom_bar_widget.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_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/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/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/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/extension/parsing_email_opened_properties_extension.dart';
import 'package:tmail_ui_user/features/thread_detail/presentation/thread_detail_controller.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)) { if (controller.responsiveUtils.isDesktop(context)) {
return Container( bodyWidget = Container(
decoration: const BoxDecoration( decoration: const BoxDecoration(
borderRadius: BorderRadius.all(Radius.circular(20)), borderRadius: BorderRadius.all(Radius.circular(20)),
color: Colors.white, color: Colors.white,
@@ -219,7 +221,17 @@ class ThreadDetailView extends GetWidget<ThreadDetailController> {
child: bodyWidget, child: bodyWidget,
); );
} else { } 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 ResponsiveUtils responsiveUtils;
final ImagePaths imagePaths; final ImagePaths imagePaths;
final bool isSearchRunning; final bool isSearchRunning;
final void Function(BuildContext context) closeThreadDetailAction; final VoidCallback closeThreadDetailAction;
final bool threadActionReady; final bool threadActionReady;
final bool threadDetailIsStarred; final bool threadDetailIsStarred;
final bool isThreadDetailEnabled; final bool isThreadDetailEnabled;
@@ -99,7 +99,7 @@ class ThreadDetailAppBar extends StatelessWidget {
builder: (_, constraints) { builder: (_, constraints) {
Widget backButton = EmailViewBackButton( Widget backButton = EmailViewBackButton(
imagePaths: imagePaths, imagePaths: imagePaths,
onBackAction: () => closeThreadDetailAction(context), onBackAction: closeThreadDetailAction,
mailboxContain: mailboxContain, mailboxContain: mailboxContain,
isSearchActivated: isSearchRunning, isSearchActivated: isSearchRunning,
maxWidth: constraints.maxWidth, maxWidth: constraints.maxWidth,
@@ -164,7 +164,7 @@ class ThreadDetailAppBar extends StatelessWidget {
return PopScope( return PopScope(
canPop: false, canPop: false,
onPopInvokedWithResult: (_, __) { onPopInvokedWithResult: (_, __) {
closeThreadDetailAction(context); closeThreadDetailAction();
}, },
child: child, child: child,
); );
@@ -7,6 +7,8 @@ import 'package:model/model.dart';
extension ListPresentationEmailExtension on List<PresentationEmail> { extension ListPresentationEmailExtension on List<PresentationEmail> {
bool get isAllEmailRead => every((email) => email.hasRead); bool get isAllEmailRead => every((email) => email.hasRead);
bool get isAllEmailUnread => every((email) => !email.hasRead);
bool get isAllEmailStarred => every((email) => email.hasStarred); bool get isAllEmailStarred => every((email) => email.hasStarred);
bool get isAllSelectionInActive { bool get isAllSelectionInActive {
@@ -15,6 +15,7 @@ import 'package:model/email/eml_attachment.dart';
import 'package:model/email/presentation_email.dart'; import 'package:model/email/presentation_email.dart';
import 'package:model/extensions/email_address_extension.dart'; import 'package:model/extensions/email_address_extension.dart';
import 'package:model/extensions/list_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/extensions/utc_date_extension.dart';
import 'package:model/mailbox/presentation_mailbox.dart'; import 'package:model/mailbox/presentation_mailbox.dart';
import 'package:model/mailbox/select_mode.dart'; import 'package:model/mailbox/select_mode.dart';
@@ -207,4 +208,13 @@ extension PresentationEmailExtension on PresentationEmail {
keywords?.addAll(newKeywords); keywords?.addAll(newKeywords);
keywords?.removeWhere((key, value) => !value); 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;
}
} }