TF-2666 Apply new UI for edit recipient card on mobile

Signed-off-by: dab246 <tdvu@linagora.com>
This commit is contained in:
dab246
2025-07-25 12:59:09 +07:00
committed by Dat H. Pham
parent 7f7dba14fb
commit 84616f210c
17 changed files with 503 additions and 254 deletions
@@ -102,6 +102,8 @@ class ThemeUtils {
fontFamily: ConstantsUI.fontApp,
fontWeight: FontWeight.w500,
letterSpacing: 0.1,
fontSize: 14,
height: 20 / 14,
),
);
}
@@ -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 {
@@ -142,6 +142,7 @@ class ComposerView extends GetWidget<ComposerController> {
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<ComposerController> {
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<ComposerController> {
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<ComposerController> {
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<ComposerController> {
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<ComposerController> {
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<ComposerController> {
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<ComposerController> {
onSuggestionEmailAddress: controller.getAutoCompleteSuggestion,
onFocusNextAddressAction: controller.handleFocusNextAddressAction,
onEditRecipientAction: controller.onEditRecipient,
onClearFocusAction: controller.onClearFocusAction,
),
],
)),
@@ -122,6 +122,7 @@ class ComposerView extends GetWidget<ComposerController> {
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<ComposerController> {
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<ComposerController> {
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<ComposerController> {
onFocusNextAddressAction: controller.handleFocusNextAddressAction,
onRemoveDraggableEmailAddressAction: controller.removeDraggableEmailAddress,
onEditRecipientAction: controller.onEditRecipient,
onClearFocusAction: controller.onClearFocusAction,
),
],
)),
@@ -395,6 +399,7 @@ class ComposerView extends GetWidget<ComposerController> {
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<ComposerController> {
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<ComposerController> {
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<ComposerController> {
onFocusNextAddressAction: controller.handleFocusNextAddressAction,
onRemoveDraggableEmailAddressAction: controller.removeDraggableEmailAddress,
onEditRecipientAction: controller.onEditRecipient,
onClearFocusAction: controller.onClearFocusAction,
),
],
)),
@@ -711,6 +719,7 @@ class ComposerView extends GetWidget<ComposerController> {
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<ComposerController> {
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<ComposerController> {
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<ComposerController> {
onFocusNextAddressAction: controller.handleFocusNextAddressAction,
onRemoveDraggableEmailAddressAction: controller.removeDraggableEmailAddress,
onEditRecipientAction: controller.onEditRecipient,
onClearFocusAction: controller.onClearFocusAction,
),
],
)),
@@ -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();
}
}
}
@@ -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<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);
}
@@ -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;
@@ -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<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);
}
@@ -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,
);
}
}
}
@@ -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<Widget> _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;
}
}
}
@@ -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,
),
),
],
),
),
);
}
}
@@ -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,
),
),
],
),
),
),
);
}
}
@@ -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<RecipientComposerWidget> {
final _responsiveUtils = Get.find<ResponsiveUtils>();
Timer? _gapBetweenTagChangedAndFindSuggestion;
bool _lastTagFocused = false;
bool _isDragging = false;
@@ -224,9 +229,11 @@ class _RecipientComposerWidgetState extends State<RecipientComposerWidget> {
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<RecipientComposerWidget> {
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),
@@ -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,
),
],
),
@@ -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,
+1 -7
View File
@@ -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",
@@ -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',