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

Signed-off-by: dab246 <tdvu@linagora.com>
This commit is contained in:
dab246
2024-12-02 14:09:27 +07:00
committed by Dat H. Pham
parent 36c386bb90
commit 3b96eeeb20
9 changed files with 398 additions and 1 deletions
@@ -0,0 +1,150 @@
import 'dart:async';
import 'package:core/utils/platform_info.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart' hide OverlayEntry;
import 'package:universal_html/html.dart' as html;
typedef OnRightMouseClickAction = void Function(RelativeRect position);
typedef OnDoubleClickAction = void Function(RelativeRect position);
class SmartInteractionWidget extends StatefulWidget {
final Widget child;
final OnRightMouseClickAction onRightMouseClickAction;
final OnDoubleClickAction onDoubleClickAction;
const SmartInteractionWidget({
super.key,
required this.child,
required this.onRightMouseClickAction,
required this.onDoubleClickAction,
});
@override
State<SmartInteractionWidget> createState() => _SmartInteractionWidgetState();
}
class _SmartInteractionWidgetState extends State<SmartInteractionWidget> {
final GlobalKey _childKey = GlobalKey();
Offset? _lastPointerEventOffset;
StreamSubscription<html.MouseEvent>? _contextMenuSubscription;
@override
void initState() {
super.initState();
if (PlatformInfo.isWeb) {
WidgetsBinding.instance.addPostFrameCallback((_) {
_addWebContextMenuListener();
});
}
}
void _addWebContextMenuListener() {
_contextMenuSubscription =
html.document.onContextMenu.listen(_onContextMenuListener);
}
void _onContextMenuListener(html.MouseEvent event) {
final renderBox =
_childKey.currentContext?.findRenderObject() as RenderBox?;
if (renderBox == null) return;
final localPosition = Offset(
event.page.x.toDouble(),
event.page.y.toDouble(),
);
final renderBoxPosition = renderBox.localToGlobal(Offset.zero);
final size = renderBox.size;
final rect = Rect.fromLTWH(
renderBoxPosition.dx,
renderBoxPosition.dy,
size.width,
size.height,
);
if (rect.contains(localPosition)) {
event.preventDefault();
if (!mounted) return;
final relativeRect = _getRelativeRectFromOffset(context, localPosition);
widget.onRightMouseClickAction(relativeRect);
}
}
RelativeRect _getRelativeRectFromOffset(
BuildContext context,
Offset offset,
) {
final widgetBox =
_childKey.currentContext?.findRenderObject() as RenderBox?;
final overlayBox =
Overlay.maybeOf(context)?.context.findRenderObject() as RenderBox?;
if (widgetBox == null || overlayBox == null) {
final screenSize = MediaQuery.sizeOf(context);
return RelativeRect.fromLTRB(
offset.dx,
offset.dy,
screenSize.width - offset.dx,
screenSize.height - offset.dy,
);
} else {
final targetPosition = widgetBox.localToGlobal(
Offset.zero,
ancestor: overlayBox,
);
final overlaySize = overlayBox.size;
const topSpace = 4.0;
return RelativeRect.fromLTRB(
targetPosition.dx,
targetPosition.dy + widgetBox.size.height + topSpace,
overlaySize.width + targetPosition.dx,
overlaySize.height - targetPosition.dy - widgetBox.size.height,
);
}
}
void _handleDoubleTap() {
if (_lastPointerEventOffset != null) {
final relativeRect = _getRelativeRectFromOffset(
context,
_lastPointerEventOffset!,
);
widget.onDoubleClickAction(relativeRect);
}
}
@override
Widget build(BuildContext context) {
if (PlatformInfo.isWeb) {
return Listener(
key: _childKey,
onPointerDown: (event) {
if (event.kind == PointerDeviceKind.mouse ||
event.kind == PointerDeviceKind.touch) {
_lastPointerEventOffset = event.position;
}
},
child: GestureDetector(
onDoubleTap: _handleDoubleTap,
child: widget.child,
),
);
} else {
return widget.child;
}
}
@override
void dispose() {
_lastPointerEventOffset = null;
if (PlatformInfo.isWeb) {
_contextMenuSubscription?.cancel();
}
super.dispose();
}
}
@@ -6,6 +6,7 @@ import 'package:model/email/prefix_email_address.dart';
import 'package:pointer_interceptor/pointer_interceptor.dart';
import 'package:tmail_ui_user/features/composer/presentation/composer_controller.dart';
import 'package:tmail_ui_user/features/composer/presentation/extensions/composer_print_draft_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/mark_as_important_extension.dart';
import 'package:tmail_ui_user/features/composer/presentation/extensions/remove_draggable_email_address_between_recipient_fields_extension.dart';
import 'package:tmail_ui_user/features/composer/presentation/model/prefix_recipient_state.dart';
@@ -120,6 +121,7 @@ class ComposerView extends GetWidget<ComposerController> {
onSuggestionEmailAddress: controller.getAutoCompleteSuggestion,
onFocusNextAddressAction: controller.handleFocusNextAddressAction,
onRemoveDraggableEmailAddressAction: controller.removeDraggableEmailAddress,
onEditRecipientAction: controller.onEditRecipient,
),
if (controller.ccRecipientState.value == PrefixRecipientState.enabled)
RecipientComposerWidget(
@@ -145,6 +147,7 @@ class ComposerView extends GetWidget<ComposerController> {
onSuggestionEmailAddress: controller.getAutoCompleteSuggestion,
onFocusNextAddressAction: controller.handleFocusNextAddressAction,
onRemoveDraggableEmailAddressAction: controller.removeDraggableEmailAddress,
onEditRecipientAction: controller.onEditRecipient,
),
if (controller.bccRecipientState.value == PrefixRecipientState.enabled)
RecipientComposerWidget(
@@ -170,6 +173,7 @@ class ComposerView extends GetWidget<ComposerController> {
onSuggestionEmailAddress: controller.getAutoCompleteSuggestion,
onFocusNextAddressAction: controller.handleFocusNextAddressAction,
onRemoveDraggableEmailAddressAction: controller.removeDraggableEmailAddress,
onEditRecipientAction: controller.onEditRecipient,
),
if (controller.replyToRecipientState.value == PrefixRecipientState.enabled)
RecipientComposerWidget(
@@ -194,6 +198,7 @@ class ComposerView extends GetWidget<ComposerController> {
onSuggestionEmailAddress: controller.getAutoCompleteSuggestion,
onFocusNextAddressAction: controller.handleFocusNextAddressAction,
onRemoveDraggableEmailAddressAction: controller.removeDraggableEmailAddress,
onEditRecipientAction: controller.onEditRecipient,
),
],
)),
@@ -389,6 +394,7 @@ class ComposerView extends GetWidget<ComposerController> {
onSuggestionEmailAddress: controller.getAutoCompleteSuggestion,
onFocusNextAddressAction: controller.handleFocusNextAddressAction,
onRemoveDraggableEmailAddressAction: controller.removeDraggableEmailAddress,
onEditRecipientAction: controller.onEditRecipient,
),
if (controller.ccRecipientState.value == PrefixRecipientState.enabled)
RecipientComposerWidget(
@@ -414,6 +420,7 @@ class ComposerView extends GetWidget<ComposerController> {
onSuggestionEmailAddress: controller.getAutoCompleteSuggestion,
onFocusNextAddressAction: controller.handleFocusNextAddressAction,
onRemoveDraggableEmailAddressAction: controller.removeDraggableEmailAddress,
onEditRecipientAction: controller.onEditRecipient,
),
if (controller.bccRecipientState.value == PrefixRecipientState.enabled)
RecipientComposerWidget(
@@ -439,6 +446,7 @@ class ComposerView extends GetWidget<ComposerController> {
onSuggestionEmailAddress: controller.getAutoCompleteSuggestion,
onFocusNextAddressAction: controller.handleFocusNextAddressAction,
onRemoveDraggableEmailAddressAction: controller.removeDraggableEmailAddress,
onEditRecipientAction: controller.onEditRecipient,
),
if (controller.replyToRecipientState.value == PrefixRecipientState.enabled)
RecipientComposerWidget(
@@ -463,6 +471,7 @@ class ComposerView extends GetWidget<ComposerController> {
onSuggestionEmailAddress: controller.getAutoCompleteSuggestion,
onFocusNextAddressAction: controller.handleFocusNextAddressAction,
onRemoveDraggableEmailAddressAction: controller.removeDraggableEmailAddress,
onEditRecipientAction: controller.onEditRecipient,
),
],
)),
@@ -701,6 +710,7 @@ class ComposerView extends GetWidget<ComposerController> {
onSuggestionEmailAddress: controller.getAutoCompleteSuggestion,
onFocusNextAddressAction: controller.handleFocusNextAddressAction,
onRemoveDraggableEmailAddressAction: controller.removeDraggableEmailAddress,
onEditRecipientAction: controller.onEditRecipient,
),
if (controller.ccRecipientState.value == PrefixRecipientState.enabled)
RecipientComposerWidget(
@@ -726,6 +736,7 @@ class ComposerView extends GetWidget<ComposerController> {
onSuggestionEmailAddress: controller.getAutoCompleteSuggestion,
onFocusNextAddressAction: controller.handleFocusNextAddressAction,
onRemoveDraggableEmailAddressAction: controller.removeDraggableEmailAddress,
onEditRecipientAction: controller.onEditRecipient,
),
if (controller.bccRecipientState.value == PrefixRecipientState.enabled)
RecipientComposerWidget(
@@ -751,6 +762,7 @@ class ComposerView extends GetWidget<ComposerController> {
onSuggestionEmailAddress: controller.getAutoCompleteSuggestion,
onFocusNextAddressAction: controller.handleFocusNextAddressAction,
onRemoveDraggableEmailAddressAction: controller.removeDraggableEmailAddress,
onEditRecipientAction: controller.onEditRecipient,
),
if (controller.replyToRecipientState.value == PrefixRecipientState.enabled)
RecipientComposerWidget(
@@ -775,6 +787,7 @@ class ComposerView extends GetWidget<ComposerController> {
onSuggestionEmailAddress: controller.getAutoCompleteSuggestion,
onFocusNextAddressAction: controller.handleFocusNextAddressAction,
onRemoveDraggableEmailAddressAction: controller.removeDraggableEmailAddress,
onEditRecipientAction: controller.onEditRecipient,
),
],
)),
@@ -0,0 +1,128 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.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/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/main/localizations/app_localizations.dart';
import 'package:tmail_ui_user/main/routes/route_navigation.dart';
extension HandleEditRecipientExtension on ComposerController {
void onEditRecipient(
BuildContext context,
PrefixEmailAddress prefix,
EmailAddress emailAddress,
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();
openPopupMenuAction(context, position, popupMenuItems);
}
void _handleEmailAddressActionTypeClick(
BuildContext context,
EmailAddressActionType actionType,
PrefixEmailAddress prefix,
EmailAddress emailAddress,
) {
switch (actionType) {
case EmailAddressActionType.copy:
_copyEmailAddress(context, emailAddress);
break;
case EmailAddressActionType.modify:
_modifyEmailAddress(prefix, emailAddress);
break;
}
}
void _copyEmailAddress(BuildContext context, EmailAddress emailAddress) {
Clipboard.setData(ClipboardData(text: emailAddress.emailAddress));
appToast.showToastSuccessMessage(
context,
AppLocalizations.of(context).email_address_copied_to_clipboard,
);
}
void _modifyEmailAddress(
PrefixEmailAddress prefix,
EmailAddress emailAddress,
) {
switch(prefix) {
case PrefixEmailAddress.to:
listToEmailAddress.remove(emailAddress);
toAddressExpandMode.value = ExpandMode.EXPAND;
toAddressExpandMode.refresh();
toEmailAddressController.text = emailAddress.emailAddress;
toEmailAddressController.value = toEmailAddressController.value.copyWith(
text: emailAddress.emailAddress,
selection: TextSelection(
baseOffset: emailAddress.emailAddress.length,
extentOffset: emailAddress.emailAddress.length,
),
composing: TextRange.empty,
);
toAddressFocusNode?.requestFocus();
break;
case PrefixEmailAddress.cc:
listCcEmailAddress.remove(emailAddress);
ccAddressExpandMode.value = ExpandMode.EXPAND;
ccAddressExpandMode.refresh();
ccEmailAddressController.text = emailAddress.emailAddress;
ccEmailAddressController.value = ccEmailAddressController.value.copyWith(
text: emailAddress.emailAddress,
selection: TextSelection(
baseOffset: emailAddress.emailAddress.length,
extentOffset: emailAddress.emailAddress.length,
),
composing: TextRange.empty,
);
ccAddressFocusNode?.requestFocus();
break;
case PrefixEmailAddress.bcc:
listBccEmailAddress.remove(emailAddress);
bccAddressExpandMode.value = ExpandMode.EXPAND;
bccAddressExpandMode.refresh();
bccEmailAddressController.text = emailAddress.emailAddress;
bccEmailAddressController.value = bccEmailAddressController.value.copyWith(
text: emailAddress.emailAddress,
selection: TextSelection(
baseOffset: emailAddress.emailAddress.length,
extentOffset: emailAddress.emailAddress.length,
),
composing: TextRange.empty,
);
bccAddressFocusNode?.requestFocus();
break;
default:
break;
}
}
}
@@ -0,0 +1,26 @@
import 'package:core/presentation/resources/image_paths.dart';
import 'package:tmail_ui_user/main/localizations/app_localizations.dart';
enum EmailAddressActionType {
copy,
modify;
String getContextMenuTitle(AppLocalizations appLocalizations) {
switch(this) {
case EmailAddressActionType.copy:
return appLocalizations.copy;
case EmailAddressActionType.modify:
return appLocalizations.modifyEmailAddress;
}
}
String getContextMenuIcon(ImagePaths imagePaths) {
switch(this) {
case EmailAddressActionType.copy:
return imagePaths.icCopy;
case EmailAddressActionType.modify:
return imagePaths.icEditRule;
}
}
}
@@ -0,0 +1,22 @@
import 'package:core/presentation/resources/image_paths.dart';
import 'package:tmail_ui_user/features/base/model/popup_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 PopupMenuItemEmailAddressActionType
extends PopupMenuItemActionRequiredIcon<EmailAddressActionType> {
final AppLocalizations appLocalizations;
final ImagePaths imagePaths;
PopupMenuItemEmailAddressActionType(
super.action,
this.appLocalizations,
this.imagePaths,
);
@override
String get actionIcon => action.getContextMenuIcon(imagePaths);
@override
String get actionName => action.getContextMenuTitle(appLocalizations);
}
@@ -37,6 +37,12 @@ typedef OnFocusEmailAddressChangeAction = void Function(PrefixEmailAddress prefi
typedef OnRemoveDraggableEmailAddressAction = void Function(DraggableEmailAddress draggableEmailAddress);
typedef OnDeleteTagAction = void Function(EmailAddress emailAddress);
typedef OnEnableAllRecipientsInputAction = void Function(bool isEnabled);
typedef OnEditRecipientAction = void Function(
BuildContext context,
PrefixEmailAddress prefix,
EmailAddress emailAddress,
RelativeRect position,
);
class RecipientComposerWidget extends StatefulWidget {
@@ -62,6 +68,7 @@ class RecipientComposerWidget extends StatefulWidget {
final OnShowFullListEmailAddressAction? onShowFullListEmailAddressAction;
final OnFocusEmailAddressChangeAction? onFocusEmailAddressChangeAction;
final OnRemoveDraggableEmailAddressAction? onRemoveDraggableEmailAddressAction;
final OnEditRecipientAction? onEditRecipientAction;
final VoidCallback? onFocusNextAddressAction;
final EdgeInsetsGeometry? padding;
final EdgeInsetsGeometry? margin;
@@ -101,6 +108,7 @@ class RecipientComposerWidget extends StatefulWidget {
this.onRemoveDraggableEmailAddressAction,
this.onEnableAllRecipientsInputAction,
this.focusNodeKeyboard,
this.onEditRecipientAction,
});
@override
@@ -217,6 +225,7 @@ class _RecipientComposerWidgetState extends State<RecipientComposerWidget> {
maxWidth: widget.maxWidth,
onDeleteTagAction: (emailAddress) => _handleDeleteTagAction.call(emailAddress, stateSetter),
onShowFullAction: widget.onShowFullListEmailAddressAction,
onEditRecipientAction: widget.onEditRecipientAction,
);
},
onTagChanged: (value) => _handleOnTagChangeAction.call(value, stateSetter),
@@ -309,6 +318,7 @@ class _RecipientComposerWidgetState extends State<RecipientComposerWidget> {
maxWidth: widget.maxWidth,
onDeleteTagAction: (emailAddress) => _handleDeleteTagAction.call(emailAddress, stateSetter),
onShowFullAction: widget.onShowFullListEmailAddressAction,
onEditRecipientAction: widget.onEditRecipientAction,
);
},
onTagChanged: (value) => _handleOnTagChangeAction.call(value, stateSetter),
@@ -10,6 +10,7 @@ import 'package:flutter_svg/flutter_svg.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:tmail_ui_user/features/base/widget/smart_interaction_widget.dart';
import 'package:tmail_ui_user/features/composer/presentation/model/draggable_email_address.dart';
import 'package:tmail_ui_user/features/composer/presentation/styles/recipient_tag_item_widget_style.dart';
import 'package:tmail_ui_user/features/composer/presentation/widgets/draggable_recipient_tag_widget.dart';
@@ -30,6 +31,7 @@ class RecipientTagItemWidget extends StatelessWidget {
final List<EmailAddress> collapsedListEmailAddress;
final OnShowFullListEmailAddressAction? onShowFullAction;
final OnDeleteTagAction? onDeleteTagAction;
final OnEditRecipientAction? onEditRecipientAction;
final bool isTestingForWeb;
final String? composerId;
@@ -47,6 +49,7 @@ class RecipientTagItemWidget extends StatelessWidget {
this.isLatestEmail = false,
this.onShowFullAction,
this.onDeleteTagAction,
this.onEditRecipientAction,
this.maxWidth,
this.composerId,
});
@@ -100,7 +103,17 @@ class RecipientTagItemWidget extends StatelessWidget {
childWhenDragging: DraggableRecipientTagWidget(emailAddress: currentEmailAddress),
child: MouseRegion(
cursor: SystemMouseCursors.grab,
child: tagWidget,
child: SmartInteractionWidget(
onRightMouseClickAction: (position) => _onEditRecipientAction(
context,
position,
),
onDoubleClickAction: (position) => _onEditRecipientAction(
context,
position,
),
child: tagWidget,
),
),
);
}
@@ -171,4 +184,13 @@ class RecipientTagItemWidget extends StatelessWidget {
);
}
}
void _onEditRecipientAction(BuildContext context, RelativeRect position) {
onEditRecipientAction?.call(
context,
prefix,
currentEmailAddress,
position,
);
}
}
+12
View File
@@ -4601,5 +4601,17 @@
"placeholders": {
"days": {}
}
},
"copy": "Copy",
"@copy": {
"type": "text",
"placeholders_order": [],
"placeholders": {}
},
"modifyEmailAddress": "Modify email address",
"@modifyEmailAddress": {
"type": "text",
"placeholders_order": [],
"placeholders": {}
}
}
@@ -4848,4 +4848,18 @@ class AppLocalizations {
args: [days],
);
}
String get copy {
return Intl.message(
'Copy',
name: 'copy',
);
}
String get modifyEmailAddress {
return Intl.message(
'Modify email address',
name: 'modifyEmailAddress',
);
}
}