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/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';
@@ -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<HtmlContentViewerOnWeb>
} 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<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
void didUpdateWidget(covariant HtmlContentViewerOnWeb oldWidget) {
super.didUpdateWidget(oldWidget);
@@ -397,6 +420,8 @@ class _HtmlContentViewerOnWebState extends State<HtmlContentViewerOnWeb>
viewId: _createdViewId,
onScrollChangedEvent: onScrollChangedEvent,
),
if (widget.onIFrameKeyboardShortcutAction != null)
HtmlInteraction.scriptHandleIframeKeyboardListener(_createdViewId),
].join();
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 = '''
<script>
<script type="text/javascript">
const bodyResizeObserver = new ResizeObserver(entries => {
window.flutter_inappwebview.callHandler('$contentSizeChangedEventJSChannelName', '');
})
@@ -70,7 +70,7 @@ class HtmlInteraction {
''';
static const String scriptsHandleLazyLoadingBackgroundImage = '''
<script>
<script type="text/javascript">
const lazyImages = document.querySelectorAll('[lazy]');
const lazyImageObserver = new IntersectionObserver((entries, observer) => {
entries.forEach((entry) => {
@@ -378,4 +378,19 @@ class HtmlInteraction {
</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,
);
}
}
@@ -106,3 +106,13 @@ class OpenAttachmentListAction extends EmailUIAction {
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_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;
@@ -51,6 +51,7 @@ class EmailView extends GetWidget<SingleEmailController> {
this.isFirstEmailInThreadDetail = false,
this.threadSubject,
this.onToggleThreadDetailCollapseExpand,
this.onIFrameKeyboardShortcutAction,
this.scrollController,
});
@@ -59,6 +60,7 @@ class EmailView extends GetWidget<SingleEmailController> {
final bool isFirstEmailInThreadDetail;
final String? threadSubject;
final VoidCallback? onToggleThreadDetailCollapseExpand;
final OnIFrameKeyboardShortcutAction? onIFrameKeyboardShortcutAction;
final ScrollController? scrollController;
@override
@@ -83,7 +85,7 @@ class EmailView extends GetWidget<SingleEmailController> {
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<SingleEmailController> {
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<SingleEmailController> {
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<SingleEmailController> {
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<SingleEmailController> {
scrollController: scrollController,
enableQuoteToggle: true,
fontSize: isMobileResponsive ? 16 : 14,
onIFrameKeyboardShortcutAction: onIFrameKeyboardShortcutAction,
),
);
} 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);
}
}
@@ -156,3 +156,5 @@ class SynchronizeEmailSortOrderAction extends DashBoardAction {
@override
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_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<Either<Failure, Success>> _progressStateController =
StreamController<Either<Failure, Success>>.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();
@@ -47,6 +47,7 @@ class SearchController extends BaseController with DateRangePickerMixin {
final listFilterOnSuggestionForm = RxList<QuickSearchFilter>();
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();
}
@@ -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/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) {
@@ -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(() {
@@ -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() {
@@ -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;
}
@@ -52,3 +52,7 @@ class LoadThreadDetailAfterSelectedEmailAction extends ThreadDetailUIAction {
@override
List<Object?> get props => [threadId, isSentMailbox];
}
class ReclaimMailViewKeyboardShortcutFocusAction extends ThreadDetailUIAction {}
class ClearMailViewKeyboardShortcutFocusAction extends ThreadDetailUIAction {}
@@ -1,10 +1,9 @@
import 'package:flutter/material.dart';
import 'package:tmail_ui_user/features/email/presentation/action/email_ui_action.dart';
import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/model/dashboard_routes.dart';
import 'package:tmail_ui_user/features/thread_detail/presentation/thread_detail_controller.dart';
extension CloseThreadDetailAction on ThreadDetailController {
void closeThreadDetailAction(BuildContext? context) {
void closeThreadDetailAction() {
if (isSearchRunning) {
mailboxDashBoardController.dispatchRoute(DashboardRoutes.searchEmail);
} else {
@@ -1,12 +1,13 @@
import 'package:core/utils/platform_info.dart';
import 'package:flutter/material.dart';
import 'package:model/email/email_in_thread_status.dart';
import 'package:model/extensions/presentation_email_extension.dart';
import 'package:tmail_ui_user/features/email/presentation/email_view.dart';
import 'package:model/email/email_in_thread_status.dart';
import 'package:tmail_ui_user/features/thread_detail/presentation/extension/handle_mail_shortcut_actions_extension.dart';
import 'package:tmail_ui_user/features/thread_detail/presentation/extension/load_more_thread_detail_emails.dart';
import 'package:tmail_ui_user/features/thread_detail/presentation/extension/thread_detail_load_more_segments.dart';
import 'package:tmail_ui_user/features/thread_detail/presentation/extension/thread_detail_on_email_action_click.dart';
import 'package:tmail_ui_user/features/thread_detail/presentation/extension/thread_detail_open_email_address_detail_action.dart';
import 'package:tmail_ui_user/features/thread_detail/presentation/extension/thread_detail_load_more_segments.dart';
import 'package:tmail_ui_user/features/thread_detail/presentation/extension/toggle_thread_detail_collapse_expand.dart';
import 'package:tmail_ui_user/features/thread_detail/presentation/thread_detail_controller.dart';
import 'package:tmail_ui_user/features/thread_detail/presentation/widgets/thread_detail_collapsed_email.dart';
@@ -78,6 +79,9 @@ extension GetThreadDetailEmailViews on ThreadDetailController {
onToggleThreadDetailCollapseExpand: () {
toggleThreadDetailCollapseExpand(presentationEmail);
},
onIFrameKeyboardShortcutAction: keyboardShortcutFocusNode != null
? onIFrameKeyboardShortcutAction
: null,
scrollController: scrollController,
);
}
@@ -91,6 +95,9 @@ extension GetThreadDetailEmailViews on ThreadDetailController {
onToggleThreadDetailCollapseExpand: () {
toggleThreadDetailCollapseExpand(presentationEmail);
},
onIFrameKeyboardShortcutAction: keyboardShortcutFocusNode != null
? onIFrameKeyboardShortcutAction
: null,
scrollController: scrollController,
);
}).toList();
@@ -0,0 +1,104 @@
import 'dart:async';
import 'package:core/presentation/views/shortcut/key_shortcut.dart';
import 'package:core/utils/app_logger.dart';
import 'package:core/utils/platform_info.dart';
import 'package:flutter/material.dart';
import 'package:model/email/presentation_email.dart';
import 'package:rxdart/rxdart.dart';
import 'package:tmail_ui_user/features/base/shortcut/app_shortcut_manager.dart';
import 'package:tmail_ui_user/features/base/shortcut/mail/mail_view_action_shortcut_type.dart';
import 'package:tmail_ui_user/features/email/presentation/action/email_ui_action.dart';
import 'package:tmail_ui_user/features/thread_detail/presentation/extension/key_shortcut_extension.dart';
import 'package:tmail_ui_user/features/thread_detail/presentation/model/mail_view_shortcut_action_view_event.dart';
import 'package:tmail_ui_user/features/thread_detail/presentation/thread_detail_controller.dart';
extension HandleMailShortcutActionsExtension on ThreadDetailController {
void onKeyDownEventAction(KeyEvent event) {
final shortcutType = AppShortcutManager.getMailViewActionFromEvent(event);
log('$runtimeType::onKeyDownEventAction:🔥Shortcut triggered: $shortcutType');
if (shortcutType == null) return;
handleMailShortcutAction(shortcutType);
}
PresentationEmail? get currentExpandedEmail {
final expandedEmailId = currentExpandedEmailId.value;
if (expandedEmailId == null) return null;
final expandedPresentationEmail = emailIdsPresentation[expandedEmailId];
if (expandedPresentationEmail == null) return null;
return expandedPresentationEmail;
}
void onKeyboardShortcutInit() {
if (PlatformInfo.isWeb) {
keyboardShortcutFocusNode = FocusNode();
keyboardShortcutFocusNode?.addListener(() {
log('$runtimeType::onInit:keyboardShortcutFocusNode: ${keyboardShortcutFocusNode?.hasFocus}');
});
shortcutActionEventController =
StreamController<MailViewShortcutActionViewEvent>();
shortcutActionEventSubscription = shortcutActionEventController
?.stream
.debounceTime(const Duration(milliseconds: 300))
.listen(_onShortcutActionViewEvent);
}
}
void _onShortcutActionViewEvent(MailViewShortcutActionViewEvent event) {
mailboxDashBoardController.dispatchEmailUIAction(
TriggerMailViewKeyboardShortcutAction(
event.actionType,
event.presentationEmail,
),
);
}
void onKeyboardShortcutDispose() {
if (PlatformInfo.isWeb) {
keyboardShortcutFocusNode?.dispose();
shortcutActionEventSubscription?.cancel();
shortcutActionEventController?.close();
}
}
void handleMailShortcutAction(MailViewActionShortcutType shortcutType) {
log('$runtimeType::handleMailShortcutAction:🔥ShortcutType: $shortcutType');
final expandedEmail = currentExpandedEmail;
if (expandedEmail == null) return;
final emailActionType = shortcutType.getEmailActionType(
currentEmail: expandedEmail,
ownerEmailAddress: mailboxDashBoardController.ownEmailAddress.value,
);
log('$runtimeType::onKeyDownEventAction:🔥EmailActionType: $emailActionType');
if (emailActionType == null) return;
shortcutActionEventController?.add(
MailViewShortcutActionViewEvent(emailActionType, expandedEmail),
);
}
void clearMailShortcutFocus() {
if (keyboardShortcutFocusNode?.hasFocus == true) {
keyboardShortcutFocusNode?.unfocus();
}
}
void refocusMailShortcutFocus() {
if (keyboardShortcutFocusNode?.hasFocus == false) {
keyboardShortcutFocusNode?.requestFocus();
}
}
void onIFrameKeyboardShortcutAction(KeyShortcut keyShortcut) {
log('$runtimeType::onIFrameKeyboardShortcutAction: ${keyShortcut.toString()}');
final shortcutType = keyShortcut.mailViewActionShortcutType;
log('$runtimeType::onIFrameKeyboardShortcutAction:🔥MailViewActionShortcutType: $shortcutType');
if (shortcutType == null) return;
handleMailShortcutAction(shortcutType);
}
}
@@ -5,7 +5,6 @@ import 'package:model/email/read_actions.dart';
import 'package:tmail_ui_user/features/thread_detail/presentation/extension/close_thread_detail_action.dart';
import 'package:tmail_ui_user/features/thread_detail/presentation/extension/toggle_thread_detail_collapse_expand.dart';
import 'package:tmail_ui_user/features/thread_detail/presentation/thread_detail_controller.dart';
import 'package:tmail_ui_user/main/routes/route_navigation.dart';
extension HandleMarkMultipleEmailsReadSuccess on ThreadDetailController {
void handleMarkMultipleEmailsReadSuccess(
@@ -17,7 +16,7 @@ extension HandleMarkMultipleEmailsReadSuccess on ThreadDetailController {
if (readActions == ReadActions.markAsRead) return;
if (emailsInThreadDetailInfo.length == 1) {
closeThreadDetailAction(currentContext);
closeThreadDetailAction();
return;
}
@@ -14,7 +14,6 @@ import 'package:tmail_ui_user/features/thread_detail/domain/usecases/get_thread_
import 'package:tmail_ui_user/features/thread_detail/presentation/extension/close_thread_detail_action.dart';
import 'package:tmail_ui_user/features/thread_detail/presentation/thread_detail_controller.dart';
import 'package:tmail_ui_user/features/thread_detail/presentation/utils/thread_detail_presentation_utils.dart';
import 'package:tmail_ui_user/main/routes/route_navigation.dart';
extension HandleRefreshThreadDetailAction on ThreadDetailController {
void handleRefreshThreadDetailAction(
@@ -86,7 +85,7 @@ extension HandleRefreshThreadDetailAction on ThreadDetailController {
);
if (afterRefreshedEmailIds.isEmpty) {
closeThreadDetailAction(currentContext);
closeThreadDetailAction();
return;
}
@@ -0,0 +1,22 @@
import 'package:core/presentation/views/shortcut/key_shortcut.dart';
import 'package:tmail_ui_user/features/base/shortcut/mail/mail_view_action_shortcut_type.dart';
extension KeyShortcutExtension on KeyShortcut {
MailViewActionShortcutType? get mailViewActionShortcutType {
if (matches('r')) {
return MailViewActionShortcutType.reply;
} else if (matches('r', shift: true)) {
return MailViewActionShortcutType.replyAll;
} else if (matches('f')) {
return MailViewActionShortcutType.forward;
} else if (matches('u')) {
return MailViewActionShortcutType.markAsUnread;
} else if (matches('n')) {
return MailViewActionShortcutType.newMessage;
} else if (matches('backspace') || matches('delete')) {
return MailViewActionShortcutType.delete;
}
return null;
}
}
@@ -5,7 +5,6 @@ import 'package:tmail_ui_user/features/email/domain/state/mark_as_email_read_sta
import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/extensions/update_current_emails_flags_extension.dart';
import 'package:tmail_ui_user/features/thread_detail/presentation/extension/close_thread_detail_action.dart';
import 'package:tmail_ui_user/features/thread_detail/presentation/thread_detail_controller.dart';
import 'package:tmail_ui_user/main/routes/route_navigation.dart';
extension MarkCollapsedEmailReadSuccess on ThreadDetailController {
void markCollapsedEmailReadSuccess(MarkAsEmailReadSuccess success) {
@@ -41,7 +40,7 @@ extension MarkCollapsedEmailReadSuccess on ThreadDetailController {
) {
if (success.readActions == ReadActions.markAsRead) return;
if (emailIdsPresentation.length == 1) {
closeThreadDetailAction(currentContext);
closeThreadDetailAction();
} else {
emailIdsPresentation[success.emailId] = emailIdsPresentation
[success.emailId]
@@ -72,7 +72,7 @@ extension OnThreadDetailActionClick on ThreadDetailController {
case EmailActionType.deletePermanently:
mailboxDashBoardController.permanentDeleteMultipleEmailInThreadDetail(
emailsInThreadDetailInfo,
onConfirm: () => closeThreadDetailAction(currentContext),
onConfirm: closeThreadDetailAction,
);
break;
case EmailActionType.moveToMailbox:
@@ -183,7 +183,7 @@ extension OnThreadDetailActionClick on ThreadDetailController {
}
void _moveToMailbox(MailboxId mailboxId, EmailActionType emailActionType) {
closeThreadDetailAction(currentContext);
closeThreadDetailAction();
mailboxDashBoardController.moveMultipleEmailInThreadDetail(
emailsInThreadDetailInfo,
destinationMailboxId: mailboxId,
@@ -20,7 +20,7 @@ extension ThreadDetailOnSelectedEmailUpdated on ThreadDetailController {
BuildContext? context,
) {
if (selectedEmail?.id == null) {
closeThreadDetailAction(context);
closeThreadDetailAction();
return;
}
@@ -0,0 +1,13 @@
import 'package:core/presentation/state/success.dart';
import 'package:model/email/email_action_type.dart';
import 'package:model/email/presentation_email.dart';
class MailViewShortcutActionViewEvent extends ViewEvent {
final EmailActionType actionType;
final PresentationEmail presentationEmail;
MailViewShortcutActionViewEvent(this.actionType, this.presentationEmail);
@override
List<Object?> get props => [actionType, presentationEmail];
}
@@ -50,9 +50,11 @@ import 'package:tmail_ui_user/features/thread_detail/presentation/extension/hand
import 'package:tmail_ui_user/features/thread_detail/presentation/extension/handle_get_emails_by_ids_success.dart';
import 'package:tmail_ui_user/features/thread_detail/presentation/extension/handle_get_thread_by_id_failure.dart';
import 'package:tmail_ui_user/features/thread_detail/presentation/extension/handle_mark_multiple_emails_read_success.dart';
import 'package:tmail_ui_user/features/thread_detail/presentation/extension/handle_mail_shortcut_actions_extension.dart';
import 'package:tmail_ui_user/features/thread_detail/presentation/extension/handle_refresh_thread_detail_action.dart';
import 'package:tmail_ui_user/features/thread_detail/presentation/extension/initialize_thread_detail_emails.dart';
import 'package:tmail_ui_user/features/thread_detail/presentation/extension/thread_detail_on_selected_email_updated.dart';
import 'package:tmail_ui_user/features/thread_detail/presentation/model/mail_view_shortcut_action_view_event.dart';
import 'package:tmail_ui_user/features/thread_detail/presentation/thread_detail_manager.dart';
import 'package:tmail_ui_user/main/localizations/app_localizations.dart';
import 'package:tmail_ui_user/main/routes/route_navigation.dart';
@@ -124,6 +126,9 @@ class ThreadDetailController extends BaseController {
CreateNewEmailRuleFilterInteractor? _createNewEmailRuleFilterInteractor;
bool loadThreadOnThreadChanged = false;
bool isDisplayAllAttachments = false;
FocusNode? keyboardShortcutFocusNode;
StreamController<MailViewShortcutActionViewEvent>? shortcutActionEventController;
StreamSubscription<MailViewShortcutActionViewEvent>? shortcutActionEventSubscription;
AccountId? get accountId => mailboxDashBoardController.accountId.value;
Session? get session => mailboxDashBoardController.sessionCurrent;
@@ -197,6 +202,10 @@ class ThreadDetailController extends BaseController {
threadId: action.threadId,
isSentMailbox: action.isSentMailbox,
);
} else if (action is ReclaimMailViewKeyboardShortcutFocusAction) {
refocusMailShortcutFocus();
} else if (action is ClearMailViewKeyboardShortcutFocusAction) {
clearMailShortcutFocus();
}
// Reset [threadDetailUIAction] to original value
mailboxDashBoardController.dispatchThreadDetailUIAction(
@@ -212,6 +221,8 @@ class ThreadDetailController extends BaseController {
);
}
});
onKeyboardShortcutInit();
}
bool _validateLoadThread(ThreadId? threadId) {
@@ -295,4 +306,10 @@ class ThreadDetailController extends BaseController {
}
super.handleFailureViewState(failure);
}
@override
void onClose() {
onKeyboardShortcutDispose();
super.onClose();
}
}
@@ -7,6 +7,7 @@ import 'package:dartz/dartz.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:model/mailbox/presentation_mailbox.dart';
import 'package:tmail_ui_user/features/base/widget/keyboard/keyboard_handler_wrapper.dart';
import 'package:tmail_ui_user/features/email/presentation/action/email_ui_action.dart';
import 'package:tmail_ui_user/features/email/presentation/styles/email_view_app_bar_widget_styles.dart';
import 'package:tmail_ui_user/features/email/presentation/widgets/email_view_bottom_bar_widget.dart';
@@ -17,6 +18,7 @@ import 'package:tmail_ui_user/features/thread_detail/presentation/extension/clos
import 'package:tmail_ui_user/features/thread_detail/presentation/extension/get_thread_detail_action_status.dart';
import 'package:tmail_ui_user/features/thread_detail/presentation/extension/get_thread_details_email_views.dart';
import 'package:tmail_ui_user/features/thread_detail/presentation/extension/on_thread_detail_action_click.dart';
import 'package:tmail_ui_user/features/thread_detail/presentation/extension/handle_mail_shortcut_actions_extension.dart';
import 'package:tmail_ui_user/features/thread_detail/presentation/extension/on_thread_page_changed.dart';
import 'package:tmail_ui_user/features/thread_detail/presentation/extension/parsing_email_opened_properties_extension.dart';
import 'package:tmail_ui_user/features/thread_detail/presentation/thread_detail_controller.dart';
@@ -209,7 +211,7 @@ class ThreadDetailView extends GetWidget<ThreadDetailController> {
}
if (controller.responsiveUtils.isDesktop(context)) {
return Container(
bodyWidget = Container(
decoration: const BoxDecoration(
borderRadius: BorderRadius.all(Radius.circular(20)),
color: Colors.white,
@@ -219,7 +221,17 @@ class ThreadDetailView extends GetWidget<ThreadDetailController> {
child: bodyWidget,
);
} else {
return ColoredBox(color: Colors.white, child: bodyWidget);
bodyWidget = ColoredBox(color: Colors.white, child: bodyWidget);
}
if (PlatformInfo.isWeb && controller.keyboardShortcutFocusNode != null) {
return KeyboardHandlerWrapper(
onKeyDownEventAction: controller.onKeyDownEventAction,
focusNode: controller.keyboardShortcutFocusNode!,
child: bodyWidget,
);
} else {
return bodyWidget;
}
}
@@ -35,7 +35,7 @@ class ThreadDetailAppBar extends StatelessWidget {
final ResponsiveUtils responsiveUtils;
final ImagePaths imagePaths;
final bool isSearchRunning;
final void Function(BuildContext context) closeThreadDetailAction;
final VoidCallback closeThreadDetailAction;
final bool threadActionReady;
final bool threadDetailIsStarred;
final bool isThreadDetailEnabled;
@@ -99,7 +99,7 @@ class ThreadDetailAppBar extends StatelessWidget {
builder: (_, constraints) {
Widget backButton = EmailViewBackButton(
imagePaths: imagePaths,
onBackAction: () => closeThreadDetailAction(context),
onBackAction: closeThreadDetailAction,
mailboxContain: mailboxContain,
isSearchActivated: isSearchRunning,
maxWidth: constraints.maxWidth,
@@ -164,7 +164,7 @@ class ThreadDetailAppBar extends StatelessWidget {
return PopScope(
canPop: false,
onPopInvokedWithResult: (_, __) {
closeThreadDetailAction(context);
closeThreadDetailAction();
},
child: child,
);
@@ -7,6 +7,8 @@ import 'package:model/model.dart';
extension ListPresentationEmailExtension on List<PresentationEmail> {
bool get isAllEmailRead => every((email) => email.hasRead);
bool get isAllEmailUnread => every((email) => !email.hasRead);
bool get isAllEmailStarred => every((email) => email.hasStarred);
bool get isAllSelectionInActive {
@@ -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;
}
}