diff --git a/lib/features/base/widget/smart_interaction_widget.dart b/lib/features/base/widget/smart_interaction_widget.dart index b9bd19cd6..7e2537a0c 100644 --- a/lib/features/base/widget/smart_interaction_widget.dart +++ b/lib/features/base/widget/smart_interaction_widget.dart @@ -7,17 +7,20 @@ import 'package:universal_html/html.dart' as html; typedef OnRightMouseClickAction = void Function(RelativeRect position); typedef OnDoubleClickAction = void Function(RelativeRect position); +typedef OnLongPressAction = void Function(); class SmartInteractionWidget extends StatefulWidget { final Widget child; final OnRightMouseClickAction onRightMouseClickAction; final OnDoubleClickAction onDoubleClickAction; + final OnLongPressAction onLongPressAction; const SmartInteractionWidget({ super.key, required this.child, required this.onRightMouseClickAction, required this.onDoubleClickAction, + required this.onLongPressAction, }); @override @@ -25,8 +28,8 @@ class SmartInteractionWidget extends StatefulWidget { } class _SmartInteractionWidgetState extends State { - final GlobalKey _childKey = GlobalKey(); + GlobalKey? _childKey; Offset? _lastPointerEventOffset; StreamSubscription? _contextMenuSubscription; @@ -34,6 +37,7 @@ class _SmartInteractionWidgetState extends State { void initState() { super.initState(); if (PlatformInfo.isWeb) { + _childKey = GlobalKey(); WidgetsBinding.instance.addPostFrameCallback((_) { _addWebContextMenuListener(); }); @@ -47,7 +51,7 @@ class _SmartInteractionWidgetState extends State { void _onContextMenuListener(html.MouseEvent event) { final renderBox = - _childKey.currentContext?.findRenderObject() as RenderBox?; + _childKey?.currentContext?.findRenderObject() as RenderBox?; if (renderBox == null) return; final localPosition = Offset( @@ -79,7 +83,7 @@ class _SmartInteractionWidgetState extends State { Offset offset, ) { final widgetBox = - _childKey.currentContext?.findRenderObject() as RenderBox?; + _childKey?.currentContext?.findRenderObject() as RenderBox?; final overlayBox = Overlay.maybeOf(context)?.context.findRenderObject() as RenderBox?; @@ -118,6 +122,8 @@ class _SmartInteractionWidgetState extends State { } } + void _handleLongPress() => widget.onLongPressAction(); + @override Widget build(BuildContext context) { if (PlatformInfo.isWeb) { @@ -135,14 +141,17 @@ class _SmartInteractionWidgetState extends State { ), ); } else { - return widget.child; + return GestureDetector( + onLongPress: _handleLongPress, + child: widget.child, + ); } } @override void dispose() { - _lastPointerEventOffset = null; if (PlatformInfo.isWeb) { + _lastPointerEventOffset = null; _contextMenuSubscription?.cancel(); } super.dispose(); diff --git a/lib/features/composer/presentation/composer_controller.dart b/lib/features/composer/presentation/composer_controller.dart index d043b86d0..2cdd1dd6f 100644 --- a/lib/features/composer/presentation/composer_controller.dart +++ b/lib/features/composer/presentation/composer_controller.dart @@ -1497,28 +1497,36 @@ class ComposerController extends BaseController } else { switch(prefixEmailAddress) { case PrefixEmailAddress.to: - toAddressExpandMode.value = ExpandMode.COLLAPSE; + if (mailboxDashBoardController.isPopupMenuOpened.isFalse) { + toAddressExpandMode.value = ExpandMode.COLLAPSE; + } final inputToEmail = toEmailAddressController.text; if (inputToEmail.trim().isNotEmpty) { autoCreateEmailTagForType(PrefixEmailAddress.to, inputToEmail); } break; case PrefixEmailAddress.cc: - ccAddressExpandMode.value = ExpandMode.COLLAPSE; + if (mailboxDashBoardController.isPopupMenuOpened.isFalse) { + ccAddressExpandMode.value = ExpandMode.COLLAPSE; + } final inputCcEmail = ccEmailAddressController.text; if (inputCcEmail.trim().isNotEmpty) { autoCreateEmailTagForType(PrefixEmailAddress.cc, inputCcEmail); } break; case PrefixEmailAddress.bcc: - bccAddressExpandMode.value = ExpandMode.COLLAPSE; + if (mailboxDashBoardController.isPopupMenuOpened.isFalse) { + bccAddressExpandMode.value = ExpandMode.COLLAPSE; + } final inputBccEmail = bccEmailAddressController.text; if (inputBccEmail.trim().isNotEmpty) { autoCreateEmailTagForType(PrefixEmailAddress.bcc, inputBccEmail); } break; case PrefixEmailAddress.replyTo: - replyToAddressExpandMode.value = ExpandMode.COLLAPSE; + if (mailboxDashBoardController.isPopupMenuOpened.isFalse) { + replyToAddressExpandMode.value = ExpandMode.COLLAPSE; + } final inputReplyToEmail = replyToEmailAddressController.text; if (inputReplyToEmail.trim().isNotEmpty) { autoCreateEmailTagForType(PrefixEmailAddress.replyTo, inputReplyToEmail); @@ -1783,6 +1791,9 @@ class ComposerController extends BaseController if (menuMoreOptionController?.menuIsShowing == true) { menuMoreOptionController?.hideMenu(); } + if (mailboxDashBoardController.isPopupMenuOpened.isTrue) { + popBack(); + } } void handleOnMouseDownHtmlEditorWeb() { diff --git a/lib/features/composer/presentation/composer_view.dart b/lib/features/composer/presentation/composer_view.dart index 1146caec5..b48ae199e 100644 --- a/lib/features/composer/presentation/composer_view.dart +++ b/lib/features/composer/presentation/composer_view.dart @@ -9,6 +9,7 @@ import 'package:get/get.dart'; import 'package:model/email/prefix_email_address.dart'; import 'package:tmail_ui_user/features/composer/presentation/composer_controller.dart'; import 'package:tmail_ui_user/features/composer/presentation/extensions/handle_content_height_exceeded_extension.dart'; +import 'package:tmail_ui_user/features/composer/presentation/extensions/handle_edit_recipient_extension.dart'; import 'package:tmail_ui_user/features/composer/presentation/extensions/handle_open_context_menu_extension.dart'; import 'package:tmail_ui_user/features/composer/presentation/extensions/mark_as_important_extension.dart'; import 'package:tmail_ui_user/features/composer/presentation/model/prefix_recipient_state.dart'; @@ -140,6 +141,7 @@ class ComposerView extends GetWidget { onSuggestionEmailAddress: controller.getAutoCompleteSuggestion, onFocusNextAddressAction: controller.handleFocusNextAddressAction, onEnableAllRecipientsInputAction: controller.handleEnableRecipientsInputAction, + onEditRecipientAction: controller.onEditRecipient, )), Obx(() { if (controller.ccRecipientState.value == PrefixRecipientState.enabled) { @@ -163,6 +165,7 @@ class ComposerView extends GetWidget { onUpdateListEmailAddressAction: controller.updateListEmailAddress, onSuggestionEmailAddress: controller.getAutoCompleteSuggestion, onFocusNextAddressAction: controller.handleFocusNextAddressAction, + onEditRecipientAction: controller.onEditRecipient, ); } else { return const SizedBox.shrink(); @@ -190,6 +193,7 @@ class ComposerView extends GetWidget { onUpdateListEmailAddressAction: controller.updateListEmailAddress, onSuggestionEmailAddress: controller.getAutoCompleteSuggestion, onFocusNextAddressAction: controller.handleFocusNextAddressAction, + onEditRecipientAction: controller.onEditRecipient, ); } else { return const SizedBox.shrink(); @@ -217,6 +221,7 @@ class ComposerView extends GetWidget { onSuggestionEmailAddress: controller.getAutoCompleteSuggestion, onFocusNextAddressAction: controller.handleFocusNextAddressAction, onEnableAllRecipientsInputAction: controller.handleEnableRecipientsInputAction, + onEditRecipientAction: controller.onEditRecipient, ); } else { return const SizedBox.shrink(); @@ -340,6 +345,7 @@ class ComposerView extends GetWidget { onSuggestionEmailAddress: controller.getAutoCompleteSuggestion, onFocusNextAddressAction: controller.handleFocusNextAddressAction, onEnableAllRecipientsInputAction: controller.handleEnableRecipientsInputAction, + onEditRecipientAction: controller.onEditRecipient, ), if (controller.ccRecipientState.value == PrefixRecipientState.enabled) RecipientComposerWidget( @@ -362,6 +368,7 @@ class ComposerView extends GetWidget { onUpdateListEmailAddressAction: controller.updateListEmailAddress, onSuggestionEmailAddress: controller.getAutoCompleteSuggestion, onFocusNextAddressAction: controller.handleFocusNextAddressAction, + onEditRecipientAction: controller.onEditRecipient, ), if (controller.bccRecipientState.value == PrefixRecipientState.enabled) RecipientComposerWidget( @@ -384,6 +391,7 @@ class ComposerView extends GetWidget { onUpdateListEmailAddressAction: controller.updateListEmailAddress, onSuggestionEmailAddress: controller.getAutoCompleteSuggestion, onFocusNextAddressAction: controller.handleFocusNextAddressAction, + onEditRecipientAction: controller.onEditRecipient, ), if (controller.replyToRecipientState.value == PrefixRecipientState.enabled) RecipientComposerWidget( @@ -405,6 +413,7 @@ class ComposerView extends GetWidget { onUpdateListEmailAddressAction: controller.updateListEmailAddress, onSuggestionEmailAddress: controller.getAutoCompleteSuggestion, onFocusNextAddressAction: controller.handleFocusNextAddressAction, + onEditRecipientAction: controller.onEditRecipient, ), ], )), diff --git a/lib/features/composer/presentation/extensions/handle_edit_recipient_extension.dart b/lib/features/composer/presentation/extensions/handle_edit_recipient_extension.dart index c188b3203..058856887 100644 --- a/lib/features/composer/presentation/extensions/handle_edit_recipient_extension.dart +++ b/lib/features/composer/presentation/extensions/handle_edit_recipient_extension.dart @@ -1,14 +1,17 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:get/get.dart'; import 'package:jmap_dart_client/jmap/mail/email/email_address.dart'; import 'package:model/email/prefix_email_address.dart'; import 'package:model/extensions/email_address_extension.dart'; import 'package:model/mailbox/expand_mode.dart'; import 'package:tmail_ui_user/features/base/widget/popup_menu/popup_menu_item_action_widget.dart'; import 'package:tmail_ui_user/features/composer/presentation/composer_controller.dart'; +import 'package:tmail_ui_user/features/composer/presentation/model/context_item_email_address_action_type.dart'; import 'package:tmail_ui_user/features/composer/presentation/model/email_address_action_type.dart'; import 'package:tmail_ui_user/features/composer/presentation/model/popup_menu_item_email_address_action_type.dart'; +import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/extensions/handle_open_context_menu_extension.dart'; import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; import 'package:tmail_ui_user/main/routes/route_navigation.dart'; @@ -17,31 +20,63 @@ extension HandleEditRecipientExtension on ComposerController { BuildContext context, PrefixEmailAddress prefix, EmailAddress emailAddress, - RelativeRect position, + RelativeRect? position, ) { - final popupMenuItems = EmailAddressActionType.values.map((type) { - return PopupMenuItem( - padding: EdgeInsets.zero, - child: PopupMenuItemActionWidget( - menuAction: PopupMenuItemEmailAddressActionType( - type, - AppLocalizations.of(context), - imagePaths, - ), - menuActionClick: (menuAction) { - popBack(); - _handleEmailAddressActionTypeClick( - context, - menuAction.action, - prefix, - emailAddress, - ); - }, - ), - ); - }).toList(); + if (position == null) { + clearFocus(context); - openPopupMenuAction(context, position, popupMenuItems); + final contextMenuActions = EmailAddressActionType.values + .map((action) => ContextItemEmailAddressActionType( + action, + AppLocalizations.of(context), + imagePaths + )) + .toList(); + + openBottomSheetContextMenuAction( + context: context, + itemActions: contextMenuActions, + onContextMenuActionClick: (menuAction) { + popBack(); + _handleEmailAddressActionTypeClick( + context, + menuAction.action, + prefix, + emailAddress, + ); + }, + ); + } else { + if (mailboxDashBoardController.isPopupMenuOpened.isTrue) return; + + final popupMenuItems = EmailAddressActionType.values.map((type) { + return PopupMenuItem( + padding: EdgeInsets.zero, + child: PopupMenuItemActionWidget( + menuAction: PopupMenuItemEmailAddressActionType( + type, + AppLocalizations.of(context), + imagePaths, + ), + menuActionClick: (menuAction) { + popBack(); + _handleEmailAddressActionTypeClick( + context, + menuAction.action, + prefix, + emailAddress, + ); + }, + ), + ); + }).toList(); + + mailboxDashBoardController.openPopupMenu( + context, + position, + popupMenuItems, + ); + } } void _handleEmailAddressActionTypeClick( @@ -121,6 +156,22 @@ extension HandleEditRecipientExtension on ComposerController { ); bccAddressFocusNode?.requestFocus(); break; + case PrefixEmailAddress.replyTo: + listReplyToEmailAddress.remove(emailAddress); + replyToAddressExpandMode.value = ExpandMode.EXPAND; + replyToAddressExpandMode.refresh(); + + replyToEmailAddressController.text = emailAddress.emailAddress; + replyToEmailAddressController.value = replyToEmailAddressController.value.copyWith( + text: emailAddress.emailAddress, + selection: TextSelection( + baseOffset: emailAddress.emailAddress.length, + extentOffset: emailAddress.emailAddress.length, + ), + composing: TextRange.empty, + ); + replyToAddressFocusNode?.requestFocus(); + break; default: break; } diff --git a/lib/features/composer/presentation/model/context_item_email_address_action_type.dart b/lib/features/composer/presentation/model/context_item_email_address_action_type.dart new file mode 100644 index 000000000..414d61c83 --- /dev/null +++ b/lib/features/composer/presentation/model/context_item_email_address_action_type.dart @@ -0,0 +1,22 @@ +import 'package:core/presentation/resources/image_paths.dart'; +import 'package:tmail_ui_user/features/base/widget/context_menu/context_menu_item_action.dart'; +import 'package:tmail_ui_user/features/composer/presentation/model/email_address_action_type.dart'; +import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; + +class ContextItemEmailAddressActionType + extends ContextMenuItemActionRequiredIcon { + final AppLocalizations appLocalizations; + final ImagePaths imagePaths; + + ContextItemEmailAddressActionType( + super.action, + this.appLocalizations, + this.imagePaths, + ); + + @override + String get actionIcon => action.getContextMenuIcon(imagePaths); + + @override + String get actionName => action.getContextMenuTitle(appLocalizations); +} diff --git a/lib/features/composer/presentation/widgets/recipient_composer_widget.dart b/lib/features/composer/presentation/widgets/recipient_composer_widget.dart index 25e181bf6..87b996ff6 100644 --- a/lib/features/composer/presentation/widgets/recipient_composer_widget.dart +++ b/lib/features/composer/presentation/widgets/recipient_composer_widget.dart @@ -41,7 +41,7 @@ typedef OnEditRecipientAction = void Function( BuildContext context, PrefixEmailAddress prefix, EmailAddress emailAddress, - RelativeRect position, + RelativeRect? position, ); class RecipientComposerWidget extends StatefulWidget { diff --git a/lib/features/composer/presentation/widgets/recipient_tag_item_widget.dart b/lib/features/composer/presentation/widgets/recipient_tag_item_widget.dart index cd02fb1eb..bbd0549e2 100644 --- a/lib/features/composer/presentation/widgets/recipient_tag_item_widget.dart +++ b/lib/features/composer/presentation/widgets/recipient_tag_item_widget.dart @@ -56,40 +56,51 @@ class RecipientTagItemWidget extends StatelessWidget { @override Widget build(BuildContext context) { - Widget tagWidget = Chip( - labelPadding: EdgeInsetsDirectional.symmetric( - horizontal: 4, - vertical: DirectionUtils.isDirectionRTLByHasAnyRtl(currentEmailAddress.asString()) ? 0 : 2 + Widget tagWidget = SmartInteractionWidget( + onRightMouseClickAction: (position) => _onEditRecipientAction( + context, + position: position, ), - padding: EdgeInsets.zero, - label: Text( - key: Key('label_recipient_tag_item_${prefix.name}_$index'), - currentEmailAddress.asString(), - maxLines: 1, - overflow: TextOverflow.ellipsis, - softWrap: true, + onDoubleClickAction: (position) => _onEditRecipientAction( + context, + position: position, ), - deleteIcon: SvgPicture.asset( - imagePaths.icClose, - key: Key('delete_icon_recipient_tag_item_${prefix.name}_$index'), - fit: BoxFit.fill + onLongPressAction: () => _onEditRecipientAction(context), + child: Chip( + labelPadding: EdgeInsetsDirectional.symmetric( + horizontal: 4, + vertical: DirectionUtils.isDirectionRTLByHasAnyRtl(currentEmailAddress.asString()) ? 0 : 2 + ), + padding: EdgeInsets.zero, + label: Text( + key: Key('label_recipient_tag_item_${prefix.name}_$index'), + currentEmailAddress.asString(), + maxLines: 1, + overflow: TextOverflow.ellipsis, + softWrap: true, + ), + deleteIcon: SvgPicture.asset( + imagePaths.icClose, + key: Key('delete_icon_recipient_tag_item_${prefix.name}_$index'), + fit: BoxFit.fill + ), + labelStyle: RecipientTagItemWidgetStyle.labelTextStyle, + backgroundColor: _getTagBackgroundColor(), + side: _getTagBorderSide(), + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(RecipientTagItemWidgetStyle.radius)), + ), + avatar: currentEmailAddress.displayName.isNotEmpty + ? GradientCircleAvatarIcon( + key: Key('avatar_icon_recipient_tag_item_${prefix.name}_$index'), + colors: currentEmailAddress.avatarColors, + label: currentEmailAddress.displayName.firstLetterToUpperCase, + labelFontSize: RecipientTagItemWidgetStyle.avatarLabelFontSize, + iconSize: RecipientTagItemWidgetStyle.avatarIconSize, + ) + : null, + onDeleted: () => onDeleteTagAction?.call(currentEmailAddress), ), - labelStyle: RecipientTagItemWidgetStyle.labelTextStyle, - backgroundColor: _getTagBackgroundColor(), - side: _getTagBorderSide(), - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.all(Radius.circular(RecipientTagItemWidgetStyle.radius)), - ), - avatar: currentEmailAddress.displayName.isNotEmpty - ? GradientCircleAvatarIcon( - key: Key('avatar_icon_recipient_tag_item_${prefix.name}_$index'), - colors: currentEmailAddress.avatarColors, - label: currentEmailAddress.displayName.firstLetterToUpperCase, - labelFontSize: RecipientTagItemWidgetStyle.avatarLabelFontSize, - iconSize: RecipientTagItemWidgetStyle.avatarIconSize, - ) - : null, - onDeleted: () => onDeleteTagAction?.call(currentEmailAddress), ); if (PlatformInfo.isWeb || isTestingForWeb) { @@ -103,17 +114,7 @@ class RecipientTagItemWidget extends StatelessWidget { childWhenDragging: DraggableRecipientTagWidget(emailAddress: currentEmailAddress), child: MouseRegion( cursor: SystemMouseCursors.grab, - child: SmartInteractionWidget( - onRightMouseClickAction: (position) => _onEditRecipientAction( - context, - position, - ), - onDoubleClickAction: (position) => _onEditRecipientAction( - context, - position, - ), - child: tagWidget, - ), + child: tagWidget, ), ); } @@ -185,7 +186,7 @@ class RecipientTagItemWidget extends StatelessWidget { } } - void _onEditRecipientAction(BuildContext context, RelativeRect position) { + void _onEditRecipientAction(BuildContext context, {RelativeRect? position}) { onEditRecipientAction?.call( context, prefix,