From 84616f210c0d4ff84eedf9c2c8edc5f1e63368df Mon Sep 17 00:00:00 2001 From: dab246 Date: Fri, 25 Jul 2025 12:59:09 +0700 Subject: [PATCH] TF-2666 Apply new UI for edit recipient card on mobile Signed-off-by: dab246 --- core/lib/presentation/utils/theme_utils.dart | 2 + ...d_with_smart_interaction_overlay_view.dart | 10 +- .../composer/presentation/composer_view.dart | 8 + .../presentation/composer_view_web.dart | 12 ++ .../handle_edit_recipient_extension.dart | 48 ++--- ...ontext_item_email_address_action_type.dart | 22 --- .../model/email_address_action_type.dart | 8 +- ...p_menu_item_email_address_action_type.dart | 22 --- .../view/edit_recipients_view.dart | 166 +++-------------- .../mobile/mobile_edit_recipients_view.dart | 153 ++++++++++++++++ .../web/desktop_edit_recipients_view.dart | 168 ++++++++++++++++++ .../widgets/email_address_action_widget.dart | 63 +++++++ .../widgets/recipient_composer_widget.dart | 9 + .../recipient_suggestion_item_widget.dart | 27 +-- .../widgets/recipient_tag_item_widget.dart | 24 ++- lib/l10n/intl_messages.arb | 8 +- lib/main/localizations/app_localizations.dart | 7 - 17 files changed, 503 insertions(+), 254 deletions(-) delete mode 100644 lib/features/composer/presentation/model/context_item_email_address_action_type.dart delete mode 100644 lib/features/composer/presentation/model/popup_menu_item_email_address_action_type.dart create mode 100644 lib/features/composer/presentation/view/mobile/mobile_edit_recipients_view.dart create mode 100644 lib/features/composer/presentation/view/web/desktop_edit_recipients_view.dart create mode 100644 lib/features/composer/presentation/widgets/email_address_action_widget.dart diff --git a/core/lib/presentation/utils/theme_utils.dart b/core/lib/presentation/utils/theme_utils.dart index 3eb448c2c..5788d8c8a 100644 --- a/core/lib/presentation/utils/theme_utils.dart +++ b/core/lib/presentation/utils/theme_utils.dart @@ -102,6 +102,8 @@ class ThemeUtils { fontFamily: ConstantsUI.fontApp, fontWeight: FontWeight.w500, letterSpacing: 0.1, + fontSize: 14, + height: 20 / 14, ), ); } diff --git a/lib/features/base/widget/card_with_smart_interaction_overlay_view.dart b/lib/features/base/widget/card_with_smart_interaction_overlay_view.dart index cc83b0619..02fe5a76c 100644 --- a/lib/features/base/widget/card_with_smart_interaction_overlay_view.dart +++ b/lib/features/base/widget/card_with_smart_interaction_overlay_view.dart @@ -1,3 +1,4 @@ +import 'package:core/utils/platform_info.dart'; import 'package:flutter/material.dart'; import 'package:pointer_interceptor/pointer_interceptor.dart'; import 'package:tmail_ui_user/features/base/widget/smart_interaction_widget.dart'; @@ -6,12 +7,14 @@ class CardWithSmartInteractionOverlayView extends StatefulWidget { final Widget child; final double overlayWidth; final Widget Function(VoidCallback onClose) menuBuilder; + final VoidCallback? onClearFocusAction; const CardWithSmartInteractionOverlayView({ super.key, required this.child, required this.menuBuilder, this.overlayWidth = 361, + this.onClearFocusAction, }); @override @@ -52,7 +55,10 @@ class _CardWithSmartInteractionOverlayViewState width: widget.overlayWidth, child: CompositedTransformFollower( link: _layerLink, - offset: const Offset(0, 40), + offset: Offset( + 0, + PlatformInfo.isWeb ? 40 : 50, + ), showWhenUnlinked: false, child: FadeTransition( opacity: CurvedAnimation( @@ -112,6 +118,8 @@ class _CardWithSmartInteractionOverlayViewState } void _togglePopup() { + widget.onClearFocusAction?.call(); + if (_overlayEntry == null) { _showPopup(); } else { diff --git a/lib/features/composer/presentation/composer_view.dart b/lib/features/composer/presentation/composer_view.dart index b48ae199e..1a625b132 100644 --- a/lib/features/composer/presentation/composer_view.dart +++ b/lib/features/composer/presentation/composer_view.dart @@ -142,6 +142,7 @@ class ComposerView extends GetWidget { onFocusNextAddressAction: controller.handleFocusNextAddressAction, onEnableAllRecipientsInputAction: controller.handleEnableRecipientsInputAction, onEditRecipientAction: controller.onEditRecipient, + onClearFocusAction: controller.onClearFocusAction, )), Obx(() { if (controller.ccRecipientState.value == PrefixRecipientState.enabled) { @@ -166,6 +167,7 @@ class ComposerView extends GetWidget { onSuggestionEmailAddress: controller.getAutoCompleteSuggestion, onFocusNextAddressAction: controller.handleFocusNextAddressAction, onEditRecipientAction: controller.onEditRecipient, + onClearFocusAction: controller.onClearFocusAction, ); } else { return const SizedBox.shrink(); @@ -194,6 +196,7 @@ class ComposerView extends GetWidget { onSuggestionEmailAddress: controller.getAutoCompleteSuggestion, onFocusNextAddressAction: controller.handleFocusNextAddressAction, onEditRecipientAction: controller.onEditRecipient, + onClearFocusAction: controller.onClearFocusAction, ); } else { return const SizedBox.shrink(); @@ -222,6 +225,7 @@ class ComposerView extends GetWidget { onFocusNextAddressAction: controller.handleFocusNextAddressAction, onEnableAllRecipientsInputAction: controller.handleEnableRecipientsInputAction, onEditRecipientAction: controller.onEditRecipient, + onClearFocusAction: controller.onClearFocusAction, ); } else { return const SizedBox.shrink(); @@ -346,6 +350,7 @@ class ComposerView extends GetWidget { onFocusNextAddressAction: controller.handleFocusNextAddressAction, onEnableAllRecipientsInputAction: controller.handleEnableRecipientsInputAction, onEditRecipientAction: controller.onEditRecipient, + onClearFocusAction: controller.onClearFocusAction, ), if (controller.ccRecipientState.value == PrefixRecipientState.enabled) RecipientComposerWidget( @@ -369,6 +374,7 @@ class ComposerView extends GetWidget { onSuggestionEmailAddress: controller.getAutoCompleteSuggestion, onFocusNextAddressAction: controller.handleFocusNextAddressAction, onEditRecipientAction: controller.onEditRecipient, + onClearFocusAction: controller.onClearFocusAction, ), if (controller.bccRecipientState.value == PrefixRecipientState.enabled) RecipientComposerWidget( @@ -392,6 +398,7 @@ class ComposerView extends GetWidget { onSuggestionEmailAddress: controller.getAutoCompleteSuggestion, onFocusNextAddressAction: controller.handleFocusNextAddressAction, onEditRecipientAction: controller.onEditRecipient, + onClearFocusAction: controller.onClearFocusAction, ), if (controller.replyToRecipientState.value == PrefixRecipientState.enabled) RecipientComposerWidget( @@ -414,6 +421,7 @@ class ComposerView extends GetWidget { onSuggestionEmailAddress: controller.getAutoCompleteSuggestion, onFocusNextAddressAction: controller.handleFocusNextAddressAction, onEditRecipientAction: controller.onEditRecipient, + onClearFocusAction: controller.onClearFocusAction, ), ], )), diff --git a/lib/features/composer/presentation/composer_view_web.dart b/lib/features/composer/presentation/composer_view_web.dart index a86a1a4d8..da7fd8159 100644 --- a/lib/features/composer/presentation/composer_view_web.dart +++ b/lib/features/composer/presentation/composer_view_web.dart @@ -122,6 +122,7 @@ class ComposerView extends GetWidget { onFocusNextAddressAction: controller.handleFocusNextAddressAction, onRemoveDraggableEmailAddressAction: controller.removeDraggableEmailAddress, onEditRecipientAction: controller.onEditRecipient, + onClearFocusAction: controller.onClearFocusAction, ), if (controller.ccRecipientState.value == PrefixRecipientState.enabled) RecipientComposerWidget( @@ -148,6 +149,7 @@ class ComposerView extends GetWidget { onFocusNextAddressAction: controller.handleFocusNextAddressAction, onRemoveDraggableEmailAddressAction: controller.removeDraggableEmailAddress, onEditRecipientAction: controller.onEditRecipient, + onClearFocusAction: controller.onClearFocusAction, ), if (controller.bccRecipientState.value == PrefixRecipientState.enabled) RecipientComposerWidget( @@ -174,6 +176,7 @@ class ComposerView extends GetWidget { onFocusNextAddressAction: controller.handleFocusNextAddressAction, onRemoveDraggableEmailAddressAction: controller.removeDraggableEmailAddress, onEditRecipientAction: controller.onEditRecipient, + onClearFocusAction: controller.onClearFocusAction, ), if (controller.replyToRecipientState.value == PrefixRecipientState.enabled) RecipientComposerWidget( @@ -199,6 +202,7 @@ class ComposerView extends GetWidget { onFocusNextAddressAction: controller.handleFocusNextAddressAction, onRemoveDraggableEmailAddressAction: controller.removeDraggableEmailAddress, onEditRecipientAction: controller.onEditRecipient, + onClearFocusAction: controller.onClearFocusAction, ), ], )), @@ -395,6 +399,7 @@ class ComposerView extends GetWidget { onFocusNextAddressAction: controller.handleFocusNextAddressAction, onRemoveDraggableEmailAddressAction: controller.removeDraggableEmailAddress, onEditRecipientAction: controller.onEditRecipient, + onClearFocusAction: controller.onClearFocusAction, ), if (controller.ccRecipientState.value == PrefixRecipientState.enabled) RecipientComposerWidget( @@ -421,6 +426,7 @@ class ComposerView extends GetWidget { onFocusNextAddressAction: controller.handleFocusNextAddressAction, onRemoveDraggableEmailAddressAction: controller.removeDraggableEmailAddress, onEditRecipientAction: controller.onEditRecipient, + onClearFocusAction: controller.onClearFocusAction, ), if (controller.bccRecipientState.value == PrefixRecipientState.enabled) RecipientComposerWidget( @@ -447,6 +453,7 @@ class ComposerView extends GetWidget { onFocusNextAddressAction: controller.handleFocusNextAddressAction, onRemoveDraggableEmailAddressAction: controller.removeDraggableEmailAddress, onEditRecipientAction: controller.onEditRecipient, + onClearFocusAction: controller.onClearFocusAction, ), if (controller.replyToRecipientState.value == PrefixRecipientState.enabled) RecipientComposerWidget( @@ -472,6 +479,7 @@ class ComposerView extends GetWidget { onFocusNextAddressAction: controller.handleFocusNextAddressAction, onRemoveDraggableEmailAddressAction: controller.removeDraggableEmailAddress, onEditRecipientAction: controller.onEditRecipient, + onClearFocusAction: controller.onClearFocusAction, ), ], )), @@ -711,6 +719,7 @@ class ComposerView extends GetWidget { onFocusNextAddressAction: controller.handleFocusNextAddressAction, onRemoveDraggableEmailAddressAction: controller.removeDraggableEmailAddress, onEditRecipientAction: controller.onEditRecipient, + onClearFocusAction: controller.onClearFocusAction, ), if (controller.ccRecipientState.value == PrefixRecipientState.enabled) RecipientComposerWidget( @@ -737,6 +746,7 @@ class ComposerView extends GetWidget { onFocusNextAddressAction: controller.handleFocusNextAddressAction, onRemoveDraggableEmailAddressAction: controller.removeDraggableEmailAddress, onEditRecipientAction: controller.onEditRecipient, + onClearFocusAction: controller.onClearFocusAction, ), if (controller.bccRecipientState.value == PrefixRecipientState.enabled) RecipientComposerWidget( @@ -763,6 +773,7 @@ class ComposerView extends GetWidget { onFocusNextAddressAction: controller.handleFocusNextAddressAction, onRemoveDraggableEmailAddressAction: controller.removeDraggableEmailAddress, onEditRecipientAction: controller.onEditRecipient, + onClearFocusAction: controller.onClearFocusAction, ), if (controller.replyToRecipientState.value == PrefixRecipientState.enabled) RecipientComposerWidget( @@ -788,6 +799,7 @@ class ComposerView extends GetWidget { onFocusNextAddressAction: controller.handleFocusNextAddressAction, onRemoveDraggableEmailAddressAction: controller.removeDraggableEmailAddress, onEditRecipientAction: controller.onEditRecipient, + onClearFocusAction: controller.onClearFocusAction, ), ], )), 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 e15b82df8..f5f651a93 100644 --- a/lib/features/composer/presentation/extensions/handle_edit_recipient_extension.dart +++ b/lib/features/composer/presentation/extensions/handle_edit_recipient_extension.dart @@ -7,11 +7,9 @@ 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/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/mailbox_dashboard/presentation/extensions/handle_create_new_rule_filter.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( @@ -20,38 +18,12 @@ extension HandleEditRecipientExtension on ComposerController { EmailAddress emailAddress, EmailAddressActionType emailAddressActionType, ) { - if (PlatformInfo.isMobile) { - clearFocus(context); - - 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 { - _handleEmailAddressActionTypeClick( - context, - emailAddressActionType, - prefix, - emailAddress, - ); - } + _handleEmailAddressActionTypeClick( + context, + emailAddressActionType, + prefix, + emailAddress, + ); } void _handleEmailAddressActionTypeClick( @@ -64,7 +36,7 @@ extension HandleEditRecipientExtension on ComposerController { case EmailAddressActionType.copy: _copyEmailAddress(context, emailAddress); break; - case EmailAddressActionType.modify: + case EmailAddressActionType.edit: _modifyEmailAddress(prefix, emailAddress); break; case EmailAddressActionType.createRule: @@ -151,4 +123,10 @@ extension HandleEditRecipientExtension on ComposerController { emailAddress: emailAddress, ); } + + void onClearFocusAction() { + if (PlatformInfo.isMobile) { + htmlEditorApi?.unfocus(); + } + } } \ No newline at end of file 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 deleted file mode 100644 index 414d61c83..000000000 --- a/lib/features/composer/presentation/model/context_item_email_address_action_type.dart +++ /dev/null @@ -1,22 +0,0 @@ -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/model/email_address_action_type.dart b/lib/features/composer/presentation/model/email_address_action_type.dart index d83b15bd0..308ad2137 100644 --- a/lib/features/composer/presentation/model/email_address_action_type.dart +++ b/lib/features/composer/presentation/model/email_address_action_type.dart @@ -4,15 +4,15 @@ import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; enum EmailAddressActionType { copy, - modify, + edit, createRule; String getContextMenuTitle(AppLocalizations appLocalizations) { switch(this) { case EmailAddressActionType.copy: return appLocalizations.copy; - case EmailAddressActionType.modify: - return appLocalizations.modifyEmailAddress; + case EmailAddressActionType.edit: + return appLocalizations.edit; case EmailAddressActionType.createRule: return appLocalizations.createARule; } @@ -22,7 +22,7 @@ enum EmailAddressActionType { switch(this) { case EmailAddressActionType.copy: return imagePaths.icCopy; - case EmailAddressActionType.modify: + case EmailAddressActionType.edit: return imagePaths.icEditRule; case EmailAddressActionType.createRule: return imagePaths.icQuickCreatingRule; diff --git a/lib/features/composer/presentation/model/popup_menu_item_email_address_action_type.dart b/lib/features/composer/presentation/model/popup_menu_item_email_address_action_type.dart deleted file mode 100644 index 99bdbfe49..000000000 --- a/lib/features/composer/presentation/model/popup_menu_item_email_address_action_type.dart +++ /dev/null @@ -1,22 +0,0 @@ -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 { - 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); -} diff --git a/lib/features/composer/presentation/view/edit_recipients_view.dart b/lib/features/composer/presentation/view/edit_recipients_view.dart index 81a1cf422..8e8e581ba 100644 --- a/lib/features/composer/presentation/view/edit_recipients_view.dart +++ b/lib/features/composer/presentation/view/edit_recipients_view.dart @@ -1,20 +1,14 @@ -import 'package:core/presentation/extensions/color_extension.dart'; -import 'package:core/presentation/extensions/string_extension.dart'; import 'package:core/presentation/resources/image_paths.dart'; -import 'package:core/presentation/utils/theme_utils.dart'; -import 'package:core/presentation/views/button/tmail_button_widget.dart'; -import 'package:core/presentation/views/dialog/confirm_dialog_button.dart'; import 'package:flutter/material.dart'; import 'package:jmap_dart_client/jmap/mail/email/email_address.dart'; -import 'package:model/extensions/email_address_extension.dart'; -import 'package:pointer_interceptor/pointer_interceptor.dart'; -import 'package:tmail_ui_user/features/base/widget/email_address_with_copy_widget.dart'; -import 'package:tmail_ui_user/features/base/widget/user_avatar_builder.dart'; -import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; +import 'package:tmail_ui_user/features/composer/presentation/view/mobile/mobile_edit_recipients_view.dart'; +import 'package:tmail_ui_user/features/composer/presentation/view/web/desktop_edit_recipients_view.dart'; class EditRecipientsView extends StatelessWidget { final EmailAddress emailAddress; final ImagePaths imagePaths; + final bool isMobile; + final double width; final VoidCallback onCopyAction; final VoidCallback onEditAction; final VoidCallback onCreateRuleAction; @@ -24,6 +18,8 @@ class EditRecipientsView extends StatelessWidget { super.key, required this.emailAddress, required this.imagePaths, + required this.isMobile, + required this.width, required this.onCopyAction, required this.onEditAction, required this.onCreateRuleAction, @@ -32,135 +28,25 @@ class EditRecipientsView extends StatelessWidget { @override Widget build(BuildContext context) { - return PointerInterceptor( - child: Container( - width: 361, - decoration: BoxDecoration( - borderRadius: const BorderRadius.all(Radius.circular(16)), - color: Colors.white, - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.15), - blurRadius: 8, - spreadRadius: 3, - offset: const Offset(0, 4), - ), - BoxShadow( - color: Colors.black.withOpacity(0.3), - blurRadius: 3, - offset: const Offset(0, 1), - ), - ], - ), - child: Stack( - children: [ - Padding( - padding: const EdgeInsetsDirectional.only( - start: 16, - end: 16, - top: 21, - bottom: 21, - ), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Row( - children: [ - UserAvatarBuilder( - username: emailAddress.asString().firstLetterToUpperCase, - size: 42, - textStyle: ThemeUtils.textStyleInter600().copyWith( - fontSize: 16, - height: 22 / 16, - letterSpacing: -0.41, - color: Colors.white, - ), - padding: const EdgeInsetsDirectional.only(end: 16), - ), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - if (emailAddress.displayName.isNotEmpty) - Padding( - padding: const EdgeInsetsDirectional.only(end: 24), - child: Text( - emailAddress.displayName, - style: Theme.of(context).textTheme.titleMedium?.copyWith( - color: AppColor.textPrimary, - ), - overflow: TextOverflow.ellipsis, - maxLines: 1, - ), - ), - EmailAddressWithCopyWidget( - label: emailAddress.emailAddress, - copyLabelIcon: imagePaths.icCopy, - textStyle: ThemeUtils.textStyleBodyBody2( - color: AppColor.steelGray400, - ), - copyIconMargin: const EdgeInsetsDirectional.only( - start: 7, - ), - copyIconColor: AppColor.steelGray400, - onCopyButtonAction: onCopyAction, - ), - ], - ), - ), - ], - ), - const SizedBox(height: 12), - Row( - children: [ - Flexible( - child: Container( - constraints: const BoxConstraints(minWidth: 127), - margin: const EdgeInsetsDirectional.only(end: 8), - height: 36, - child: ConfirmDialogButton( - label: AppLocalizations.of(context).editEmail, - backgroundColor: AppColor.primaryMain, - textColor: Colors.white, - onTapAction: onEditAction, - ), - ), - ), - Flexible( - child: Container( - constraints: const BoxConstraints(minWidth: 134), - height: 36, - child: ConfirmDialogButton( - label: AppLocalizations.of(context).createARule, - backgroundColor: Colors.white, - textColor: AppColor.primaryMain, - borderColor: AppColor.primaryMain, - onTapAction: onCreateRuleAction, - ), - ), - ), - ], - ) - ], - ), - ), - PositionedDirectional( - top: 0, - end: 0, - child: TMailButtonWidget.fromIcon( - icon: imagePaths.icCloseDialog, - iconSize: 24, - iconColor: AppColor.m3Tertiary, - padding: const EdgeInsets.all(10), - borderRadius: 24, - backgroundColor: Colors.transparent, - onTapActionCallback: onCloseAction, - ), - ), - ], - ), - ), - ); + if (isMobile) { + return MobileEditRecipientsView( + emailAddress: emailAddress, + imagePaths: imagePaths, + width: width, + onCopyAction: onCopyAction, + onEditAction: onEditAction, + onCreateRuleAction: onCreateRuleAction, + ); + } else { + return DesktopEditRecipientsView( + emailAddress: emailAddress, + imagePaths: imagePaths, + width: width, + onCopyAction: onCopyAction, + onEditAction: onEditAction, + onCreateRuleAction: onCreateRuleAction, + onCloseAction: onCloseAction, + ); + } } } diff --git a/lib/features/composer/presentation/view/mobile/mobile_edit_recipients_view.dart b/lib/features/composer/presentation/view/mobile/mobile_edit_recipients_view.dart new file mode 100644 index 000000000..dbde4b059 --- /dev/null +++ b/lib/features/composer/presentation/view/mobile/mobile_edit_recipients_view.dart @@ -0,0 +1,153 @@ +import 'package:core/presentation/extensions/color_extension.dart'; +import 'package:core/presentation/extensions/string_extension.dart'; +import 'package:core/presentation/resources/image_paths.dart'; +import 'package:core/presentation/utils/theme_utils.dart'; +import 'package:flutter/material.dart'; +import 'package:jmap_dart_client/jmap/mail/email/email_address.dart'; +import 'package:model/extensions/email_address_extension.dart'; +import 'package:pointer_interceptor/pointer_interceptor.dart'; +import 'package:tmail_ui_user/features/base/widget/user_avatar_builder.dart'; +import 'package:tmail_ui_user/features/composer/presentation/model/email_address_action_type.dart'; +import 'package:tmail_ui_user/features/composer/presentation/widgets/email_address_action_widget.dart'; +import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; + +class MobileEditRecipientsView extends StatelessWidget { + final EmailAddress emailAddress; + final ImagePaths imagePaths; + final double width; + final VoidCallback onCopyAction; + final VoidCallback onEditAction; + final VoidCallback onCreateRuleAction; + + const MobileEditRecipientsView({ + super.key, + required this.emailAddress, + required this.imagePaths, + required this.width, + required this.onCopyAction, + required this.onEditAction, + required this.onCreateRuleAction, + }); + + @override + Widget build(BuildContext context) { + return PointerInterceptor( + child: Container( + width: width, + decoration: BoxDecoration( + borderRadius: const BorderRadius.all(Radius.circular(16)), + color: Colors.white, + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.15), + blurRadius: 8, + spreadRadius: 3, + offset: const Offset(0, 4), + ), + BoxShadow( + color: Colors.black.withOpacity(0.3), + blurRadius: 3, + offset: const Offset(0, 1), + ), + ], + ), + padding: const EdgeInsetsDirectional.only(bottom: 6), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: const EdgeInsetsDirectional.symmetric( + horizontal: 14, + vertical: 12, + ), + child: Row( + children: [ + UserAvatarBuilder( + username: emailAddress.asString().firstLetterToUpperCase, + size: 28, + textStyle: ThemeUtils.textStyleInter600().copyWith( + fontSize: 11, + height: 22 / 11, + letterSpacing: -0.41, + color: Colors.white, + ), + padding: const EdgeInsetsDirectional.only(end: 16), + ), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + if (emailAddress.displayName.isNotEmpty) + Padding( + padding: const EdgeInsetsDirectional.only(end: 24), + child: Text( + emailAddress.displayName, + style: Theme.of(context) + .textTheme + .titleSmall + ?.copyWith( + color: AppColor.textPrimary, + ), + overflow: TextOverflow.ellipsis, + maxLines: 1, + ), + ), + Text( + emailAddress.emailAddress, + style: ThemeUtils.textStyleInter400.copyWith( + color: AppColor.steelGray400, + fontSize: 11, + height: 14 / 11, + letterSpacing: 0.0, + ), + overflow: TextOverflow.ellipsis, + maxLines: 1, + ), + ], + ), + ), + ], + ), + ), + ..._buildActions(AppLocalizations.of(context)), + ], + ), + ), + ); + } + + List _buildActions(AppLocalizations appLocalizations) { + return EmailAddressActionType.values.map((type) { + return Container( + decoration: BoxDecoration( + border: Border( + top: BorderSide( + color: AppColor.gray424244.withOpacity(0.12), + width: 1, + ), + ), + ), + child: EmailAddressActionWidget( + imagePaths: imagePaths, + actionType: type, + onClick: _handleEmailAddressActionTypeClick, + ), + ); + }).toList(); + } + + void _handleEmailAddressActionTypeClick(EmailAddressActionType actionType) { + switch (actionType) { + case EmailAddressActionType.copy: + onCopyAction(); + break; + case EmailAddressActionType.edit: + onEditAction(); + break; + case EmailAddressActionType.createRule: + onCreateRuleAction(); + break; + } + } +} diff --git a/lib/features/composer/presentation/view/web/desktop_edit_recipients_view.dart b/lib/features/composer/presentation/view/web/desktop_edit_recipients_view.dart new file mode 100644 index 000000000..0e0552af1 --- /dev/null +++ b/lib/features/composer/presentation/view/web/desktop_edit_recipients_view.dart @@ -0,0 +1,168 @@ +import 'package:core/presentation/extensions/color_extension.dart'; +import 'package:core/presentation/extensions/string_extension.dart'; +import 'package:core/presentation/resources/image_paths.dart'; +import 'package:core/presentation/utils/theme_utils.dart'; +import 'package:core/presentation/views/button/tmail_button_widget.dart'; +import 'package:core/presentation/views/dialog/confirm_dialog_button.dart'; +import 'package:flutter/material.dart'; +import 'package:jmap_dart_client/jmap/mail/email/email_address.dart'; +import 'package:model/extensions/email_address_extension.dart'; +import 'package:pointer_interceptor/pointer_interceptor.dart'; +import 'package:tmail_ui_user/features/base/widget/email_address_with_copy_widget.dart'; +import 'package:tmail_ui_user/features/base/widget/user_avatar_builder.dart'; +import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; + +class DesktopEditRecipientsView extends StatelessWidget { + final EmailAddress emailAddress; + final ImagePaths imagePaths; + final double width; + final VoidCallback onCopyAction; + final VoidCallback onEditAction; + final VoidCallback onCreateRuleAction; + final VoidCallback onCloseAction; + + const DesktopEditRecipientsView({ + super.key, + required this.emailAddress, + required this.imagePaths, + required this.width, + required this.onCopyAction, + required this.onEditAction, + required this.onCreateRuleAction, + required this.onCloseAction, + }); + + @override + Widget build(BuildContext context) { + return PointerInterceptor( + child: Container( + width: width, + decoration: BoxDecoration( + borderRadius: const BorderRadius.all(Radius.circular(16)), + color: Colors.white, + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.15), + blurRadius: 8, + spreadRadius: 3, + offset: const Offset(0, 4), + ), + BoxShadow( + color: Colors.black.withOpacity(0.3), + blurRadius: 3, + offset: const Offset(0, 1), + ), + ], + ), + child: Stack( + children: [ + Padding( + padding: const EdgeInsetsDirectional.only( + start: 16, + end: 16, + top: 21, + bottom: 21, + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Row( + children: [ + UserAvatarBuilder( + username: emailAddress.asString().firstLetterToUpperCase, + size: 42, + textStyle: ThemeUtils.textStyleInter600().copyWith( + fontSize: 16, + height: 22 / 16, + letterSpacing: -0.41, + color: Colors.white, + ), + padding: const EdgeInsetsDirectional.only(end: 16), + ), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + if (emailAddress.displayName.isNotEmpty) + Padding( + padding: const EdgeInsetsDirectional.only(end: 24), + child: Text( + emailAddress.displayName, + style: Theme.of(context).textTheme.titleMedium?.copyWith( + color: AppColor.textPrimary, + ), + overflow: TextOverflow.ellipsis, + maxLines: 1, + ), + ), + EmailAddressWithCopyWidget( + label: emailAddress.emailAddress, + copyLabelIcon: imagePaths.icCopy, + textStyle: ThemeUtils.textStyleBodyBody2( + color: AppColor.steelGray400, + ), + copyIconMargin: const EdgeInsetsDirectional.only( + start: 7, + ), + copyIconColor: AppColor.steelGray400, + onCopyButtonAction: onCopyAction, + ), + ], + ), + ), + ], + ), + const SizedBox(height: 12), + Row( + children: [ + Flexible( + child: Container( + constraints: const BoxConstraints(minWidth: 127), + margin: const EdgeInsetsDirectional.only(end: 8), + height: 36, + child: ConfirmDialogButton( + label: AppLocalizations.of(context).editEmail, + backgroundColor: AppColor.primaryMain, + textColor: Colors.white, + onTapAction: onEditAction, + ), + ), + ), + Flexible( + child: Container( + constraints: const BoxConstraints(minWidth: 134), + height: 36, + child: ConfirmDialogButton( + label: AppLocalizations.of(context).createARule, + backgroundColor: Colors.white, + textColor: AppColor.primaryMain, + borderColor: AppColor.primaryMain, + onTapAction: onCreateRuleAction, + ), + ), + ), + ], + ) + ], + ), + ), + PositionedDirectional( + top: 0, + end: 0, + child: TMailButtonWidget.fromIcon( + icon: imagePaths.icCloseDialog, + iconSize: 24, + iconColor: AppColor.m3Tertiary, + padding: const EdgeInsets.all(10), + borderRadius: 24, + backgroundColor: Colors.transparent, + onTapActionCallback: onCloseAction, + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/features/composer/presentation/widgets/email_address_action_widget.dart b/lib/features/composer/presentation/widgets/email_address_action_widget.dart new file mode 100644 index 000000000..817c72ae4 --- /dev/null +++ b/lib/features/composer/presentation/widgets/email_address_action_widget.dart @@ -0,0 +1,63 @@ +import 'package:core/presentation/extensions/color_extension.dart'; +import 'package:core/presentation/resources/image_paths.dart'; +import 'package:core/presentation/utils/theme_utils.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.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'; + +typedef OnClickEmailAddressAction = void Function(EmailAddressActionType actionType); + +class EmailAddressActionWidget extends StatelessWidget { + final ImagePaths imagePaths; + final EmailAddressActionType actionType; + final OnClickEmailAddressAction onClick; + + const EmailAddressActionWidget({ + super.key, + required this.imagePaths, + required this.actionType, + required this.onClick, + }); + + @override + Widget build(BuildContext context) { + return Material( + type: MaterialType.transparency, + clipBehavior: Clip.antiAlias, + child: InkWell( + onTap: () => onClick(actionType), + child: Container( + width: double.infinity, + height: 36, + padding: const EdgeInsets.symmetric(horizontal: 14), + child: Row( + children: [ + SvgPicture.asset( + actionType.getContextMenuIcon(imagePaths), + width: 20, + height: 20, + colorFilter: AppColor.steelGrayA540.asFilter(), + fit: BoxFit.fill, + ), + const SizedBox(width: 22), + Expanded( + child: Text( + actionType.getContextMenuTitle(AppLocalizations.of(context)), + style: ThemeUtils.textStyleInter400.copyWith( + fontSize: 14, + height: 21.01 / 14, + letterSpacing: -0.15, + color: AppColor.gray424244.withOpacity(0.9), + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/features/composer/presentation/widgets/recipient_composer_widget.dart b/lib/features/composer/presentation/widgets/recipient_composer_widget.dart index 6a575c9cf..6cf1dc1b4 100644 --- a/lib/features/composer/presentation/widgets/recipient_composer_widget.dart +++ b/lib/features/composer/presentation/widgets/recipient_composer_widget.dart @@ -12,6 +12,7 @@ import 'package:core/utils/platform_info.dart'; import 'package:core/utils/string_convert.dart'; 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'; @@ -77,6 +78,7 @@ class RecipientComposerWidget extends StatefulWidget { final bool isTestingForWeb; final int minInputLengthAutocomplete; final String? composerId; + final VoidCallback? onClearFocusAction; const RecipientComposerWidget({ super.key, @@ -110,6 +112,7 @@ class RecipientComposerWidget extends StatefulWidget { this.onEnableAllRecipientsInputAction, this.focusNodeKeyboard, this.onEditRecipientAction, + this.onClearFocusAction, }); @override @@ -118,6 +121,8 @@ class RecipientComposerWidget extends StatefulWidget { class _RecipientComposerWidgetState extends State { + final _responsiveUtils = Get.find(); + Timer? _gapBetweenTagChangedAndFindSuggestion; bool _lastTagFocused = false; bool _isDragging = false; @@ -224,9 +229,11 @@ class _RecipientComposerWidgetState extends State { isCollapsed: _isCollapse, isLatestTagFocused: _lastTagFocused, maxWidth: widget.maxWidth, + isMobile: _responsiveUtils.isMobile(context), onDeleteTagAction: (emailAddress) => _handleDeleteTagAction.call(emailAddress, stateSetter), onShowFullAction: widget.onShowFullListEmailAddressAction, onEditRecipientAction: widget.onEditRecipientAction, + onClearFocusAction: widget.onClearFocusAction, ); }, onTagChanged: (value) => _handleOnTagChangeAction.call(value, stateSetter), @@ -317,9 +324,11 @@ class _RecipientComposerWidgetState extends State { isCollapsed: _isCollapse, isLatestTagFocused: _lastTagFocused, maxWidth: widget.maxWidth, + isMobile: _responsiveUtils.isMobile(context), onDeleteTagAction: (emailAddress) => _handleDeleteTagAction.call(emailAddress, stateSetter), onShowFullAction: widget.onShowFullListEmailAddressAction, onEditRecipientAction: widget.onEditRecipientAction, + onClearFocusAction: widget.onClearFocusAction, ); }, onTagChanged: (value) => _handleOnTagChangeAction.call(value, stateSetter), diff --git a/lib/features/composer/presentation/widgets/recipient_suggestion_item_widget.dart b/lib/features/composer/presentation/widgets/recipient_suggestion_item_widget.dart index a03a90f46..853fd1d44 100644 --- a/lib/features/composer/presentation/widgets/recipient_suggestion_item_widget.dart +++ b/lib/features/composer/presentation/widgets/recipient_suggestion_item_widget.dart @@ -1,7 +1,6 @@ import 'package:core/presentation/extensions/color_extension.dart'; import 'package:core/presentation/resources/image_paths.dart'; -import 'package:core/presentation/views/text/rich_text_builder.dart'; import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:jmap_dart_client/jmap/mail/email/email_address.dart'; @@ -50,7 +49,6 @@ class RecipientSuggestionItemWidget extends StatelessWidget { child: Padding( padding: RecipientSuggestionItemWidgetStyle.labelPadding, child: Row( - mainAxisAlignment: MainAxisAlignment.center, children: [ AvatarSuggestionItemWidget(emailAddress: emailAddress), const SizedBox(width: 8), @@ -58,18 +56,22 @@ class RecipientSuggestionItemWidget extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.min, children: [ RichTextWidget( textOrigin: emailAddress.asString(), wordSearched: suggestionValid ?? '', overflow: TextOverflow.ellipsis, + maxLines: 1, ), if (emailAddress.displayName.isNotEmpty) - RichTextBuilder( + RichTextWidget( textOrigin: emailAddress.emailAddress, - wordToStyle: suggestionValid ?? '', - styleOrigin: RecipientSuggestionItemWidgetStyle.labelTextStyle, - styleWord: RecipientSuggestionItemWidgetStyle.labelHighlightTextStyle, + wordSearched: suggestionValid ?? '', + overflow: TextOverflow.ellipsis, + maxLines: 1, + styleTextOrigin: RecipientSuggestionItemWidgetStyle.labelTextStyle, + styleWordSearched: RecipientSuggestionItemWidgetStyle.labelHighlightTextStyle, ), ], ), @@ -98,7 +100,6 @@ class RecipientSuggestionItemWidget extends StatelessWidget { child: Padding( padding: RecipientSuggestionItemWidgetStyle.labelPadding, child: Row( - mainAxisAlignment: MainAxisAlignment.center, children: [ AvatarSuggestionItemWidget(emailAddress: emailAddress), const SizedBox(width: 8), @@ -106,18 +107,22 @@ class RecipientSuggestionItemWidget extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.min, children: [ RichTextWidget( textOrigin: emailAddress.asString(), wordSearched: suggestionValid ?? '', overflow: TextOverflow.ellipsis, + maxLines: 1, ), if (emailAddress.displayName.isNotEmpty) - RichTextBuilder( + RichTextWidget( textOrigin: emailAddress.emailAddress, - wordToStyle: suggestionValid ?? '', - styleOrigin: RecipientSuggestionItemWidgetStyle.labelTextStyle, - styleWord: RecipientSuggestionItemWidgetStyle.labelHighlightTextStyle, + wordSearched: suggestionValid ?? '', + overflow: TextOverflow.ellipsis, + maxLines: 1, + styleTextOrigin: RecipientSuggestionItemWidgetStyle.labelTextStyle, + styleWordSearched: RecipientSuggestionItemWidgetStyle.labelHighlightTextStyle, ), ], ), 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 9cb713c4b..5e11773b1 100644 --- a/lib/features/composer/presentation/widgets/recipient_tag_item_widget.dart +++ b/lib/features/composer/presentation/widgets/recipient_tag_item_widget.dart @@ -36,6 +36,8 @@ class RecipientTagItemWidget extends StatelessWidget { final OnEditRecipientAction? onEditRecipientAction; final bool isTestingForWeb; final String? composerId; + final bool isMobile; + final VoidCallback? onClearFocusAction; const RecipientTagItemWidget({ super.key, @@ -49,28 +51,39 @@ class RecipientTagItemWidget extends StatelessWidget { this.isCollapsed = false, this.isLatestTagFocused = false, this.isLatestEmail = false, + this.isMobile = false, this.onShowFullAction, this.onDeleteTagAction, this.onEditRecipientAction, this.maxWidth, this.composerId, + this.onClearFocusAction, }); @override Widget build(BuildContext context) { + final overlayWidth = isMobile ? 227.0 : 361.0; Widget tagWidget = CardWithSmartInteractionOverlayView( + overlayWidth: overlayWidth, menuBuilder: (onClose) => EditRecipientsView( emailAddress: currentEmailAddress, imagePaths: imagePaths, - onCopyAction: () => _onEditRecipientAction( - context, - EmailAddressActionType.copy, - ), + isMobile: isMobile, + width: overlayWidth, + onCopyAction: () { + if (isMobile) { + onClose(); + } + _onEditRecipientAction( + context, + EmailAddressActionType.copy, + ); + }, onEditAction: () { onClose(); _onEditRecipientAction( context, - EmailAddressActionType.modify, + EmailAddressActionType.edit, ); }, onCreateRuleAction: () { @@ -82,6 +95,7 @@ class RecipientTagItemWidget extends StatelessWidget { }, onCloseAction: onClose, ), + onClearFocusAction: onClearFocusAction, child: Chip( labelPadding: EdgeInsetsDirectional.symmetric( horizontal: 4, diff --git a/lib/l10n/intl_messages.arb b/lib/l10n/intl_messages.arb index 71d6e67da..b84750a36 100644 --- a/lib/l10n/intl_messages.arb +++ b/lib/l10n/intl_messages.arb @@ -1,5 +1,5 @@ { - "@@last_modified": "2025-07-24T16:57:08.573149", + "@@last_modified": "2025-07-25T12:21:42.514040", "initializing_data": "Initializing data...", "@initializing_data": { "type": "text", @@ -4608,12 +4608,6 @@ "placeholders_order": [], "placeholders": {} }, - "modifyEmailAddress": "Modify email address", - "@modifyEmailAddress": { - "type": "text", - "placeholders_order": [], - "placeholders": {} - }, "editEmail": "Edit email", "@editEmail": { "type": "text", diff --git a/lib/main/localizations/app_localizations.dart b/lib/main/localizations/app_localizations.dart index 372f07ca1..5835723a8 100644 --- a/lib/main/localizations/app_localizations.dart +++ b/lib/main/localizations/app_localizations.dart @@ -4856,13 +4856,6 @@ class AppLocalizations { ); } - String get modifyEmailAddress { - return Intl.message( - 'Modify email address', - name: 'modifyEmailAddress', - ); - } - String get editEmail { return Intl.message( 'Edit email',