TF-2666 Edit one recipient in To/Cc/Bcc on mobile

Signed-off-by: dab246 <tdvu@linagora.com>
This commit is contained in:
dab246
2025-07-23 15:43:26 +07:00
committed by Dat H. Pham
parent 3b96eeeb20
commit 7ca3950c34
7 changed files with 179 additions and 76 deletions
@@ -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<SmartInteractionWidget> {
final GlobalKey _childKey = GlobalKey();
GlobalKey? _childKey;
Offset? _lastPointerEventOffset;
StreamSubscription<html.MouseEvent>? _contextMenuSubscription;
@@ -34,6 +37,7 @@ class _SmartInteractionWidgetState extends State<SmartInteractionWidget> {
void initState() {
super.initState();
if (PlatformInfo.isWeb) {
_childKey = GlobalKey();
WidgetsBinding.instance.addPostFrameCallback((_) {
_addWebContextMenuListener();
});
@@ -47,7 +51,7 @@ class _SmartInteractionWidgetState extends State<SmartInteractionWidget> {
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<SmartInteractionWidget> {
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<SmartInteractionWidget> {
}
}
void _handleLongPress() => widget.onLongPressAction();
@override
Widget build(BuildContext context) {
if (PlatformInfo.isWeb) {
@@ -135,14 +141,17 @@ class _SmartInteractionWidgetState extends State<SmartInteractionWidget> {
),
);
} 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();
@@ -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() {
@@ -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<ComposerController> {
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<ComposerController> {
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<ComposerController> {
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<ComposerController> {
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<ComposerController> {
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<ComposerController> {
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<ComposerController> {
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<ComposerController> {
onUpdateListEmailAddressAction: controller.updateListEmailAddress,
onSuggestionEmailAddress: controller.getAutoCompleteSuggestion,
onFocusNextAddressAction: controller.handleFocusNextAddressAction,
onEditRecipientAction: controller.onEditRecipient,
),
],
)),
@@ -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;
}
@@ -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<EmailAddressActionType> {
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);
}
@@ -41,7 +41,7 @@ typedef OnEditRecipientAction = void Function(
BuildContext context,
PrefixEmailAddress prefix,
EmailAddress emailAddress,
RelativeRect position,
RelativeRect? position,
);
class RecipientComposerWidget extends StatefulWidget {
@@ -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,