diff --git a/core/lib/presentation/extensions/color_extension.dart b/core/lib/presentation/extensions/color_extension.dart index 68e0dddbc..733e32dfa 100644 --- a/core/lib/presentation/extensions/color_extension.dart +++ b/core/lib/presentation/extensions/color_extension.dart @@ -244,6 +244,7 @@ extension AppColor on Color { static const m3Neutral90 = Color(0xFFE6E1E5); static const m3SysLightSecondaryBlue = Color(0xFF5C9CE6); static const m3SysLight = Color(0xFF0157AD); + static const m3SysOutline = Color(0xFFAEAEC0); static const grayBackgroundColor = Color(0xFFF3F6F9); static const m3SurfaceBackground = Color(0xFF1C1B1F); static const warningColor = Color(0xFFFFC107); diff --git a/lib/features/base/widget/default_field/default_autocomplete_tag_item_widget.dart b/lib/features/base/widget/default_field/default_autocomplete_tag_item_widget.dart index 2e5b6ce7c..0afca3fd2 100644 --- a/lib/features/base/widget/default_field/default_autocomplete_tag_item_widget.dart +++ b/lib/features/base/widget/default_field/default_autocomplete_tag_item_widget.dart @@ -7,8 +7,8 @@ import 'package:flutter_svg/flutter_svg.dart'; import 'package:jmap_dart_client/jmap/mail/email/email_address.dart'; import 'package:model/extensions/email_address_extension.dart'; import 'package:tmail_ui_user/features/base/widget/default_field/default_autocomplete_input_field_widget.dart'; +import 'package:tmail_ui_user/features/base/widget/default_field/default_draggable_tag_widget.dart'; import 'package:tmail_ui_user/features/composer/presentation/model/draggable_email_address.dart'; -import 'package:tmail_ui_user/features/composer/presentation/widgets/draggable_recipient_tag_widget.dart'; import 'package:tmail_ui_user/features/base/model/filter_filter.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/styles/autocomplete_tag_item_web_style.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/widgets/advanced_search/avatar_tag_item_widget.dart'; @@ -71,11 +71,11 @@ class DefaultAutocompleteTagItemWidget extends StatelessWidget { emailAddress: currentEmailAddress, filterField: field, ), - feedback: DraggableRecipientTagWidget( + feedback: DefaultDraggableTagWidget( emailAddress: currentEmailAddress, padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 4), ), - childWhenDragging: DraggableRecipientTagWidget( + childWhenDragging: DefaultDraggableTagWidget( emailAddress: currentEmailAddress, padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 4), ), diff --git a/lib/features/base/widget/default_field/default_draggable_tag_widget.dart b/lib/features/base/widget/default_field/default_draggable_tag_widget.dart new file mode 100644 index 000000000..e931affef --- /dev/null +++ b/lib/features/base/widget/default_field/default_draggable_tag_widget.dart @@ -0,0 +1,67 @@ +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/views/avatar/gradient_circle_avatar_icon.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:get/get.dart'; +import 'package:jmap_dart_client/jmap/mail/email/email_address.dart'; +import 'package:model/extensions/email_address_extension.dart'; +import 'package:tmail_ui_user/features/base/widget/default_field/default_draggable_tag_widget_style.dart'; + +class DefaultDraggableTagWidget extends StatelessWidget { + final EmailAddress emailAddress; + final EdgeInsetsGeometry? padding; + + final _imagePaths = Get.find(); + + DefaultDraggableTagWidget({ + super.key, + required this.emailAddress, + this.padding, + }); + + @override + Widget build(BuildContext context) { + return MouseRegion( + cursor: SystemMouseCursors.grab, + child: Container( + decoration: const ShapeDecoration( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.all( + Radius.circular(DefaultDraggableTagWidgetStyle.radius), + ), + ), + color: DefaultDraggableTagWidgetStyle.backgroundColor, + ), + padding: padding ?? DefaultDraggableTagWidgetStyle.padding, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + if (emailAddress.emailAddress.isNotEmpty) + GradientCircleAvatarIcon( + colors: emailAddress.avatarColors, + label: emailAddress.emailAddress.firstCharacterToUpperCase, + labelFontSize: + DefaultDraggableTagWidgetStyle.avatarLabelFontSize, + iconSize: DefaultDraggableTagWidgetStyle.avatarIconSize, + ), + Padding( + padding: DefaultDraggableTagWidgetStyle.labelPadding, + child: DefaultTextStyle( + style: DefaultDraggableTagWidgetStyle.labelTextStyle, + child: Text(emailAddress.asString()), + ), + ), + SvgPicture.asset( + _imagePaths.icClose, + colorFilter: + DefaultDraggableTagWidgetStyle.deleteIconColor.asFilter(), + fit: BoxFit.fill, + ) + ], + ), + ), + ); + } +} diff --git a/lib/features/composer/presentation/styles/draggable_recipient_tag_widget_style.dart b/lib/features/base/widget/default_field/default_draggable_tag_widget_style.dart similarity index 94% rename from lib/features/composer/presentation/styles/draggable_recipient_tag_widget_style.dart rename to lib/features/base/widget/default_field/default_draggable_tag_widget_style.dart index c68c19157..26b20fea6 100644 --- a/lib/features/composer/presentation/styles/draggable_recipient_tag_widget_style.dart +++ b/lib/features/base/widget/default_field/default_draggable_tag_widget_style.dart @@ -3,7 +3,7 @@ import 'package:core/presentation/extensions/color_extension.dart'; import 'package:core/presentation/utils/theme_utils.dart'; import 'package:flutter/material.dart'; -class DraggableRecipientTagWidgetStyle { +class DefaultDraggableTagWidgetStyle { static const double radius = 10; static const double avatarIconSize = 24; static const double avatarLabelFontSize = 12; diff --git a/lib/features/base/widget/smart_interaction_widget.dart b/lib/features/base/widget/smart_interaction_widget.dart index 707d1b539..cfa930d33 100644 --- a/lib/features/base/widget/smart_interaction_widget.dart +++ b/lib/features/base/widget/smart_interaction_widget.dart @@ -149,20 +149,26 @@ class _SmartInteractionWidgetState extends State { _lastPointerEventOffset = event.position; } }, + child: MouseRegion( + cursor: SystemMouseCursors.click, + child: GestureDetector( + onDoubleTap: _handleDoubleTap, + onTap: _handleOnTapAction, + child: widget.child, + ), + ), + ); + } else { + return MouseRegion( + cursor: SystemMouseCursors.click, child: GestureDetector( + key: _childKey, + behavior: HitTestBehavior.opaque, onDoubleTap: _handleDoubleTap, onTap: _handleOnTapAction, child: widget.child, ), ); - } else { - return GestureDetector( - key: _childKey, - behavior: HitTestBehavior.opaque, - onDoubleTap: _handleDoubleTap, - onTap: _handleOnTapAction, - child: widget.child, - ); } } else { return GestureDetector( diff --git a/lib/features/composer/presentation/composer_controller.dart b/lib/features/composer/presentation/composer_controller.dart index fa726c8a4..b1fdc550d 100644 --- a/lib/features/composer/presentation/composer_controller.dart +++ b/lib/features/composer/presentation/composer_controller.dart @@ -62,6 +62,7 @@ import 'package:tmail_ui_user/features/composer/presentation/extensions/get_draf import 'package:tmail_ui_user/features/composer/presentation/extensions/get_outbox_mailbox_id_for_composer_extension.dart'; import 'package:tmail_ui_user/features/composer/presentation/extensions/get_sent_mailbox_id_for_composer_extension.dart'; import 'package:tmail_ui_user/features/composer/presentation/extensions/handle_message_failure_extension.dart'; +import 'package:tmail_ui_user/features/composer/presentation/extensions/handle_recipients_collapsed_extensions.dart'; import 'package:tmail_ui_user/features/composer/presentation/extensions/list_identities_extension.dart'; import 'package:tmail_ui_user/features/composer/presentation/extensions/sanitize_signature_in_email_content_extension.dart'; import 'package:tmail_ui_user/features/composer/presentation/extensions/setup_email_attachments_extension.dart'; @@ -96,7 +97,6 @@ import 'package:tmail_ui_user/features/email/domain/usecases/save_template_email import 'package:tmail_ui_user/features/email/domain/usecases/transform_html_email_content_interactor.dart'; import 'package:tmail_ui_user/features/email/presentation/extensions/presentation_email_extension.dart'; import 'package:tmail_ui_user/features/email/presentation/model/composer_arguments.dart'; -import 'package:tmail_ui_user/features/email/presentation/utils/email_utils.dart'; import 'package:tmail_ui_user/features/mailbox/domain/model/create_new_mailbox_request.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/domain/usecases/remove_composer_cache_by_id_on_web_interactor.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart'; @@ -110,8 +110,8 @@ import 'package:tmail_ui_user/features/network_connection/presentation/network_c if (dart.library.html) 'package:tmail_ui_user/features/network_connection/presentation/web_network_connection_controller.dart'; import 'package:tmail_ui_user/features/server_settings/domain/usecases/get_server_setting_interactor.dart'; import 'package:tmail_ui_user/features/upload/domain/exceptions/pick_file_exception.dart'; -import 'package:tmail_ui_user/features/upload/domain/extensions/list_file_info_extension.dart'; import 'package:tmail_ui_user/features/upload/domain/extensions/file_info_extension.dart'; +import 'package:tmail_ui_user/features/upload/domain/extensions/list_file_info_extension.dart'; import 'package:tmail_ui_user/features/upload/domain/extensions/list_file_upload_extension.dart'; import 'package:tmail_ui_user/features/upload/domain/model/upload_task_id.dart'; import 'package:tmail_ui_user/features/upload/domain/state/attachment_upload_state.dart'; @@ -146,9 +146,11 @@ class ComposerController extends BaseController final emailContentsViewState = Rxn>(); final hasRequestReadReceipt = false.obs; final fromRecipientState = PrefixRecipientState.disabled.obs; + final toRecipientState = PrefixRecipientState.enabled.obs; final ccRecipientState = PrefixRecipientState.disabled.obs; final bccRecipientState = PrefixRecipientState.disabled.obs; final replyToRecipientState = PrefixRecipientState.disabled.obs; + final recipientsCollapsedState = PrefixRecipientState.disabled.obs; final identitySelected = Rxn(); final listFromIdentities = RxList(); final isEmailChanged = Rx(false); @@ -243,6 +245,7 @@ class ComposerController extends BaseController EmailActionType? savedActionType; int minInputLengthAutocomplete = AppConfig.defaultMinInputLengthAutocomplete; EmailId? currentTemplateEmailId; + PrefixEmailAddress prefixRootState = PrefixEmailAddress.to; @visibleForTesting int? get savedEmailDraftHash => _savedEmailDraftHash; @@ -601,8 +604,8 @@ class ComposerController extends BaseController } _collapseAllRecipient(); autoCreateEmailTag(); - if (PlatformInfo.isWeb) { - _hideCcBccReplyToRecipients(); + if (PlatformInfo.isWeb && isRecipientsNotEmpty) { + hideAllRecipients(); } } } @@ -722,26 +725,34 @@ class ComposerController extends BaseController listBccEmailAddress = List.from(recipients.bcc); listReplyToEmailAddress = List.from(recipients.replyTo); - if (listToEmailAddress.isNotEmpty || listCcEmailAddress.isNotEmpty || listBccEmailAddress.isNotEmpty || listReplyToEmailAddress.isNotEmpty) { - isInitialRecipient.value = true; - toAddressExpandMode.value = ExpandMode.COLLAPSE; - } + if (PlatformInfo.isWeb) { + if (isRecipientsNotEmpty) { + hideAllRecipients(); + isInitialRecipient.value = true; + } else { + toRecipientState.value = PrefixRecipientState.enabled; + } + } else { + if (isRecipientsNotEmpty) { + isInitialRecipient.value = true; + toAddressExpandMode.value = ExpandMode.COLLAPSE; + } - if (listCcEmailAddress.isNotEmpty) { - ccRecipientState.value = PrefixRecipientState.enabled; - ccAddressExpandMode.value = ExpandMode.COLLAPSE; - } + if (listCcEmailAddress.isNotEmpty) { + ccRecipientState.value = PrefixRecipientState.enabled; + ccAddressExpandMode.value = ExpandMode.COLLAPSE; + } - if (listBccEmailAddress.isNotEmpty) { - bccRecipientState.value = PrefixRecipientState.enabled; - bccAddressExpandMode.value = ExpandMode.COLLAPSE; - } + if (listBccEmailAddress.isNotEmpty) { + bccRecipientState.value = PrefixRecipientState.enabled; + bccAddressExpandMode.value = ExpandMode.COLLAPSE; + } - if (listReplyToEmailAddress.isNotEmpty) { - replyToRecipientState.value = PrefixRecipientState.enabled; - replyToAddressExpandMode.value = ExpandMode.COLLAPSE; + if (listReplyToEmailAddress.isNotEmpty) { + replyToRecipientState.value = PrefixRecipientState.enabled; + replyToAddressExpandMode.value = ExpandMode.COLLAPSE; + } } - updateStatusEmailSendButton(); } @@ -809,10 +820,6 @@ class ComposerController extends BaseController return; } - final allListEmailAddress = listToEmailAddress + listCcEmailAddress + listBccEmailAddress + listReplyToEmailAddress; - final listEmailAddressInvalid = allListEmailAddress - .where((emailAddress) => !EmailUtils.isEmailAddressValid(emailAddress.emailAddress)) - .toList(); if (listEmailAddressInvalid.isNotEmpty) { MessageDialogActionManager().showConfirmDialogAction(context, appLocalizations.message_dialog_send_email_with_email_address_invalid, @@ -1466,9 +1473,9 @@ class ComposerController extends BaseController void clickOutsideComposer(BuildContext context) { clearFocus(context); - if (PlatformInfo.isWeb) { + if (PlatformInfo.isWeb && isRecipientsNotEmpty) { _collapseAllRecipient(); - _hideCcBccReplyToRecipients(); + hideAllRecipients(); } } @@ -1501,6 +1508,9 @@ class ComposerController extends BaseController case PrefixEmailAddress.from: fromRecipientState.value = PrefixRecipientState.enabled; break; + case PrefixEmailAddress.to: + toRecipientState.value = PrefixRecipientState.enabled; + break; case PrefixEmailAddress.cc: ccRecipientState.value = PrefixRecipientState.enabled; break; @@ -1510,8 +1520,6 @@ class ComposerController extends BaseController case PrefixEmailAddress.replyTo: replyToRecipientState.value = PrefixRecipientState.enabled; break; - default: - break; } } @@ -1545,10 +1553,15 @@ class ComposerController extends BaseController replyToAddressExpandMode.value = ExpandMode.COLLAPSE; } - void _hideCcBccReplyToRecipients() { - ccRecipientState.value = PrefixRecipientState.disabled; - bccRecipientState.value = PrefixRecipientState.disabled; - replyToRecipientState.value = PrefixRecipientState.disabled; + void clearFocusRecipients() { + toAddressFocusNode?.unfocus(); + ccAddressFocusNode?.unfocus(); + bccAddressFocusNode?.unfocus(); + replyToAddressFocusNode?.unfocus(); + } + + void clearFocusSubject() { + subjectEmailInputFocusNode?.unfocus(); } void clearFocusRecipients() { @@ -1793,9 +1806,6 @@ class ComposerController extends BaseController } _collapseAllRecipient(); autoCreateEmailTag(); - if (PlatformInfo.isWeb) { - _hideCcBccReplyToRecipients(); - } } void _onChangeCursorOnMobile(List? coordinates, BuildContext context) { @@ -1926,14 +1936,14 @@ class ComposerController extends BaseController if (mailboxDashBoardController.isPopupMenuOpened.isTrue) { popBack(); } - if (PlatformInfo.isWeb) { - _hideCcBccReplyToRecipients(); + autoCreateEmailTag(); + if (PlatformInfo.isWeb && isRecipientsNotEmpty) { + hideAllRecipients(); } } void handleOnMouseDownHtmlEditorWeb() { _collapseAllRecipient(); - autoCreateEmailTag(); } FocusNode? getNextFocusOfToEmailAddress() { diff --git a/lib/features/composer/presentation/composer_view_web.dart b/lib/features/composer/presentation/composer_view_web.dart index d9351d4d1..3e0ffa08f 100644 --- a/lib/features/composer/presentation/composer_view_web.dart +++ b/lib/features/composer/presentation/composer_view_web.dart @@ -2,13 +2,16 @@ import 'package:core/presentation/views/responsive/responsive_widget.dart'; import 'package:file_picker/file_picker.dart'; import 'package:flutter/material.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/mailbox/expand_mode.dart'; import 'package:pointer_interceptor/pointer_interceptor.dart'; import 'package:tmail_ui_user/features/base/mixin/message_dialog_action_manager.dart'; import 'package:tmail_ui_user/features/base/widget/dialog_picker/color_dialog_picker.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/handle_recipients_collapsed_extensions.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'; @@ -18,6 +21,7 @@ import 'package:tmail_ui_user/features/composer/presentation/view/web/mobile_res import 'package:tmail_ui_user/features/composer/presentation/view/web/tablet_responsive_container_view.dart'; import 'package:tmail_ui_user/features/composer/presentation/view/web/web_editor_view.dart'; import 'package:tmail_ui_user/features/composer/presentation/widgets/insert_image_loading_bar_widget.dart'; +import 'package:tmail_ui_user/features/composer/presentation/widgets/list_recipients_collapsed_widget.dart'; import 'package:tmail_ui_user/features/composer/presentation/widgets/mobile/from_composer_mobile_widget.dart'; import 'package:tmail_ui_user/features/composer/presentation/widgets/recipient_composer_widget.dart'; import 'package:tmail_ui_user/features/composer/presentation/widgets/subject_composer_widget.dart'; @@ -117,115 +121,71 @@ class ComposerView extends GetWidget { onTap: () => controller.openSelectIdentityBottomSheet(context) ), ), - RecipientComposerWidget( - composerId: composerId, - prefix: PrefixEmailAddress.to, - listEmailAddress: controller.listToEmailAddress, - imagePaths: controller.imagePaths, - maxWidth: constraints.maxWidth, - minInputLengthAutocomplete: controller.minInputLengthAutocomplete, - fromState: controller.fromRecipientState.value, - ccState: controller.ccRecipientState.value, - bccState: controller.bccRecipientState.value, - replyToState: controller.replyToRecipientState.value, - expandMode: controller.toAddressExpandMode.value, - controller: controller.toEmailAddressController, - focusNode: controller.toAddressFocusNode, - focusNodeKeyboard: controller.toAddressFocusNodeKeyboard, - keyTagEditor: controller.keyToEmailTagEditor, - isInitial: controller.isInitialRecipient.value, - padding: ComposerStyle.mobileRecipientPadding, - margin: ComposerStyle.mobileRecipientMargin, - nextFocusNode: controller.getNextFocusOfToEmailAddress(), - onFocusEmailAddressChangeAction: controller.onEmailAddressFocusChange, - onShowFullListEmailAddressAction: controller.showFullEmailAddress, - onAddEmailAddressTypeAction: controller.addEmailAddressType, - onUpdateListEmailAddressAction: controller.updateListEmailAddress, - onSuggestionEmailAddress: controller.getAutoCompleteSuggestion, - onFocusNextAddressAction: controller.handleFocusNextAddressAction, - onRemoveDraggableEmailAddressAction: controller.removeDraggableEmailAddress, - onEditRecipientAction: controller.onEditRecipient, - onClearFocusAction: controller.onClearFocusAction, - ), + if (controller.recipientsCollapsedState.value == PrefixRecipientState.enabled) + RecipientsCollapsedComposerWidget( + listEmailAddress: controller.allListEmailAddress, + margin: ComposerStyle.mobileRecipientMargin, + onShowAllRecipientsAction: controller.showFullRecipients, + ), + if (controller.toRecipientState.value == PrefixRecipientState.enabled) + _buildRecipientComposerWidget( + composerId: composerId, + prefix: PrefixEmailAddress.to, + controller: controller, + maxWidth: constraints.maxWidth, + listEmailAddress: controller.listToEmailAddress, + expandMode: controller.toAddressExpandMode.value, + textController: controller.toEmailAddressController, + focusNode: controller.toAddressFocusNode, + focusNodeKeyboard: controller.toAddressFocusNodeKeyboard, + keyTagEditor: controller.keyToEmailTagEditor, + nextFocusNode: controller.getNextFocusOfToEmailAddress(), + isMobile: true, + ), if (controller.ccRecipientState.value == PrefixRecipientState.enabled) - RecipientComposerWidget( + _buildRecipientComposerWidget( composerId: composerId, prefix: PrefixEmailAddress.cc, - listEmailAddress: controller.listCcEmailAddress, - imagePaths: controller.imagePaths, + controller: controller, maxWidth: constraints.maxWidth, - minInputLengthAutocomplete: controller.minInputLengthAutocomplete, + listEmailAddress: controller.listCcEmailAddress, expandMode: controller.ccAddressExpandMode.value, - controller: controller.ccEmailAddressController, + textController: controller.ccEmailAddressController, focusNode: controller.ccAddressFocusNode, focusNodeKeyboard: controller.ccAddressFocusNodeKeyboard, keyTagEditor: controller.keyCcEmailTagEditor, - isInitial: controller.isInitialRecipient.value, nextFocusNode: controller.getNextFocusOfCcEmailAddress(), - padding: ComposerStyle.mobileRecipientPadding, - margin: ComposerStyle.mobileRecipientMargin, - onFocusEmailAddressChangeAction: controller.onEmailAddressFocusChange, - onShowFullListEmailAddressAction: controller.showFullEmailAddress, - onDeleteEmailAddressTypeAction: controller.deleteEmailAddressType, - onUpdateListEmailAddressAction: controller.updateListEmailAddress, - onSuggestionEmailAddress: controller.getAutoCompleteSuggestion, - onFocusNextAddressAction: controller.handleFocusNextAddressAction, - onRemoveDraggableEmailAddressAction: controller.removeDraggableEmailAddress, - onEditRecipientAction: controller.onEditRecipient, - onClearFocusAction: controller.onClearFocusAction, + isMobile: true, ), if (controller.bccRecipientState.value == PrefixRecipientState.enabled) - RecipientComposerWidget( + _buildRecipientComposerWidget( composerId: composerId, prefix: PrefixEmailAddress.bcc, - listEmailAddress: controller.listBccEmailAddress, - imagePaths: controller.imagePaths, + controller: controller, maxWidth: constraints.maxWidth, - minInputLengthAutocomplete: controller.minInputLengthAutocomplete, + listEmailAddress: controller.listBccEmailAddress, expandMode: controller.bccAddressExpandMode.value, - controller: controller.bccEmailAddressController, + textController: controller.bccEmailAddressController, focusNode: controller.bccAddressFocusNode, focusNodeKeyboard: controller.bccAddressFocusNodeKeyboard, keyTagEditor: controller.keyBccEmailTagEditor, - isInitial: controller.isInitialRecipient.value, - nextFocusNode: controller.getNextFocusOfCcEmailAddress(), - padding: ComposerStyle.mobileRecipientPadding, - margin: ComposerStyle.mobileRecipientMargin, - onFocusEmailAddressChangeAction: controller.onEmailAddressFocusChange, - onShowFullListEmailAddressAction: controller.showFullEmailAddress, - onDeleteEmailAddressTypeAction: controller.deleteEmailAddressType, - onUpdateListEmailAddressAction: controller.updateListEmailAddress, - onSuggestionEmailAddress: controller.getAutoCompleteSuggestion, - onFocusNextAddressAction: controller.handleFocusNextAddressAction, - onRemoveDraggableEmailAddressAction: controller.removeDraggableEmailAddress, - onEditRecipientAction: controller.onEditRecipient, - onClearFocusAction: controller.onClearFocusAction, + nextFocusNode: controller.getNextFocusOfBccEmailAddress(), + isMobile: true, ), if (controller.replyToRecipientState.value == PrefixRecipientState.enabled) - RecipientComposerWidget( + _buildRecipientComposerWidget( composerId: composerId, prefix: PrefixEmailAddress.replyTo, - listEmailAddress: controller.listReplyToEmailAddress, - imagePaths: controller.imagePaths, + controller: controller, maxWidth: constraints.maxWidth, + listEmailAddress: controller.listReplyToEmailAddress, expandMode: controller.replyToAddressExpandMode.value, - controller: controller.replyToEmailAddressController, + textController: controller.replyToEmailAddressController, focusNode: controller.replyToAddressFocusNode, focusNodeKeyboard: controller.replyToAddressFocusNodeKeyboard, keyTagEditor: controller.keyReplyToEmailTagEditor, - isInitial: controller.isInitialRecipient.value, nextFocusNode: controller.subjectEmailInputFocusNode, - padding: ComposerStyle.mobileRecipientPadding, - margin: ComposerStyle.mobileRecipientMargin, - onFocusEmailAddressChangeAction: controller.onEmailAddressFocusChange, - onShowFullListEmailAddressAction: controller.showFullEmailAddress, - onDeleteEmailAddressTypeAction: controller.deleteEmailAddressType, - onUpdateListEmailAddressAction: controller.updateListEmailAddress, - onSuggestionEmailAddress: controller.getAutoCompleteSuggestion, - onFocusNextAddressAction: controller.handleFocusNextAddressAction, - onRemoveDraggableEmailAddressAction: controller.removeDraggableEmailAddress, - onEditRecipientAction: controller.onEditRecipient, - onClearFocusAction: controller.onClearFocusAction, + isMobile: true, ), ], )), @@ -393,115 +353,67 @@ class ComposerView extends GetWidget { margin: ComposerStyle.desktopRecipientMargin, onChangeIdentity: controller.onChangeIdentity, ), - RecipientComposerWidget( - composerId: composerId, - prefix: PrefixEmailAddress.to, - listEmailAddress: controller.listToEmailAddress, - imagePaths: controller.imagePaths, - maxWidth: constraints.maxWidth, - minInputLengthAutocomplete: controller.minInputLengthAutocomplete, - fromState: controller.fromRecipientState.value, - ccState: controller.ccRecipientState.value, - bccState: controller.bccRecipientState.value, - replyToState: controller.replyToRecipientState.value, - expandMode: controller.toAddressExpandMode.value, - controller: controller.toEmailAddressController, - focusNode: controller.toAddressFocusNode, - focusNodeKeyboard: controller.toAddressFocusNodeKeyboard, - keyTagEditor: controller.keyToEmailTagEditor, - isInitial: controller.isInitialRecipient.value, - padding: ComposerStyle.desktopRecipientPadding, - margin: ComposerStyle.desktopRecipientMargin, - nextFocusNode: controller.getNextFocusOfToEmailAddress(), - onFocusEmailAddressChangeAction: controller.onEmailAddressFocusChange, - onShowFullListEmailAddressAction: controller.showFullEmailAddress, - onAddEmailAddressTypeAction: controller.addEmailAddressType, - onUpdateListEmailAddressAction: controller.updateListEmailAddress, - onSuggestionEmailAddress: controller.getAutoCompleteSuggestion, - onFocusNextAddressAction: controller.handleFocusNextAddressAction, - onRemoveDraggableEmailAddressAction: controller.removeDraggableEmailAddress, - onEditRecipientAction: controller.onEditRecipient, - onClearFocusAction: controller.onClearFocusAction, - ), + if (controller.recipientsCollapsedState.value == PrefixRecipientState.enabled) + RecipientsCollapsedComposerWidget( + listEmailAddress: controller.allListEmailAddress, + margin: ComposerStyle.desktopRecipientMargin, + onShowAllRecipientsAction: controller.showFullRecipients, + ), + if (controller.toRecipientState.value == PrefixRecipientState.enabled) + _buildRecipientComposerWidget( + composerId: composerId, + prefix: PrefixEmailAddress.to, + controller: controller, + maxWidth: constraints.maxWidth, + listEmailAddress: controller.listToEmailAddress, + expandMode: controller.toAddressExpandMode.value, + textController: controller.toEmailAddressController, + focusNode: controller.toAddressFocusNode, + focusNodeKeyboard: controller.toAddressFocusNodeKeyboard, + keyTagEditor: controller.keyToEmailTagEditor, + nextFocusNode: controller.getNextFocusOfToEmailAddress(), + ), if (controller.ccRecipientState.value == PrefixRecipientState.enabled) - RecipientComposerWidget( + _buildRecipientComposerWidget( composerId: composerId, prefix: PrefixEmailAddress.cc, - listEmailAddress: controller.listCcEmailAddress, - imagePaths: controller.imagePaths, + controller: controller, maxWidth: constraints.maxWidth, - minInputLengthAutocomplete: controller.minInputLengthAutocomplete, + listEmailAddress: controller.listCcEmailAddress, expandMode: controller.ccAddressExpandMode.value, - controller: controller.ccEmailAddressController, + textController: controller.ccEmailAddressController, focusNode: controller.ccAddressFocusNode, focusNodeKeyboard: controller.ccAddressFocusNodeKeyboard, keyTagEditor: controller.keyCcEmailTagEditor, - isInitial: controller.isInitialRecipient.value, nextFocusNode: controller.getNextFocusOfCcEmailAddress(), - padding: ComposerStyle.desktopRecipientPadding, - margin: ComposerStyle.desktopRecipientMargin, - onFocusEmailAddressChangeAction: controller.onEmailAddressFocusChange, - onShowFullListEmailAddressAction: controller.showFullEmailAddress, - onDeleteEmailAddressTypeAction: controller.deleteEmailAddressType, - onUpdateListEmailAddressAction: controller.updateListEmailAddress, - onSuggestionEmailAddress: controller.getAutoCompleteSuggestion, - onFocusNextAddressAction: controller.handleFocusNextAddressAction, - onRemoveDraggableEmailAddressAction: controller.removeDraggableEmailAddress, - onEditRecipientAction: controller.onEditRecipient, - onClearFocusAction: controller.onClearFocusAction, ), if (controller.bccRecipientState.value == PrefixRecipientState.enabled) - RecipientComposerWidget( + _buildRecipientComposerWidget( composerId: composerId, prefix: PrefixEmailAddress.bcc, - listEmailAddress: controller.listBccEmailAddress, - imagePaths: controller.imagePaths, + controller: controller, maxWidth: constraints.maxWidth, - minInputLengthAutocomplete: controller.minInputLengthAutocomplete, + listEmailAddress: controller.listBccEmailAddress, expandMode: controller.bccAddressExpandMode.value, - controller: controller.bccEmailAddressController, + textController: controller.bccEmailAddressController, focusNode: controller.bccAddressFocusNode, focusNodeKeyboard: controller.bccAddressFocusNodeKeyboard, keyTagEditor: controller.keyBccEmailTagEditor, - isInitial: controller.isInitialRecipient.value, nextFocusNode: controller.getNextFocusOfBccEmailAddress(), - padding: ComposerStyle.desktopRecipientPadding, - margin: ComposerStyle.desktopRecipientMargin, - onFocusEmailAddressChangeAction: controller.onEmailAddressFocusChange, - onShowFullListEmailAddressAction: controller.showFullEmailAddress, - onDeleteEmailAddressTypeAction: controller.deleteEmailAddressType, - onUpdateListEmailAddressAction: controller.updateListEmailAddress, - onSuggestionEmailAddress: controller.getAutoCompleteSuggestion, - onFocusNextAddressAction: controller.handleFocusNextAddressAction, - onRemoveDraggableEmailAddressAction: controller.removeDraggableEmailAddress, - onEditRecipientAction: controller.onEditRecipient, - onClearFocusAction: controller.onClearFocusAction, ), if (controller.replyToRecipientState.value == PrefixRecipientState.enabled) - RecipientComposerWidget( + _buildRecipientComposerWidget( composerId: composerId, prefix: PrefixEmailAddress.replyTo, - listEmailAddress: controller.listReplyToEmailAddress, - imagePaths: controller.imagePaths, + controller: controller, maxWidth: constraints.maxWidth, + listEmailAddress: controller.listReplyToEmailAddress, expandMode: controller.replyToAddressExpandMode.value, - controller: controller.replyToEmailAddressController, + textController: controller.replyToEmailAddressController, focusNode: controller.replyToAddressFocusNode, focusNodeKeyboard: controller.replyToAddressFocusNodeKeyboard, keyTagEditor: controller.keyReplyToEmailTagEditor, - isInitial: controller.isInitialRecipient.value, nextFocusNode: controller.subjectEmailInputFocusNode, - padding: ComposerStyle.desktopRecipientPadding, - margin: ComposerStyle.desktopRecipientMargin, - onFocusEmailAddressChangeAction: controller.onEmailAddressFocusChange, - onShowFullListEmailAddressAction: controller.showFullEmailAddress, - onDeleteEmailAddressTypeAction: controller.deleteEmailAddressType, - onUpdateListEmailAddressAction: controller.updateListEmailAddress, - onSuggestionEmailAddress: controller.getAutoCompleteSuggestion, - onFocusNextAddressAction: controller.handleFocusNextAddressAction, - onRemoveDraggableEmailAddressAction: controller.removeDraggableEmailAddress, - onEditRecipientAction: controller.onEditRecipient, - onClearFocusAction: controller.onClearFocusAction, ), ], )), @@ -714,115 +626,67 @@ class ComposerView extends GetWidget { margin: ComposerStyle.tabletRecipientMargin, onChangeIdentity: controller.onChangeIdentity, ), - RecipientComposerWidget( - composerId: composerId, - prefix: PrefixEmailAddress.to, - listEmailAddress: controller.listToEmailAddress, - imagePaths: controller.imagePaths, - maxWidth: constraints.maxWidth, - minInputLengthAutocomplete: controller.minInputLengthAutocomplete, - fromState: controller.fromRecipientState.value, - ccState: controller.ccRecipientState.value, - bccState: controller.bccRecipientState.value, - replyToState: controller.replyToRecipientState.value, - expandMode: controller.toAddressExpandMode.value, - controller: controller.toEmailAddressController, - focusNode: controller.toAddressFocusNode, - focusNodeKeyboard: controller.toAddressFocusNodeKeyboard, - keyTagEditor: controller.keyToEmailTagEditor, - isInitial: controller.isInitialRecipient.value, - padding: ComposerStyle.tabletRecipientPadding, - margin: ComposerStyle.tabletRecipientMargin, - nextFocusNode: controller.getNextFocusOfToEmailAddress(), - onFocusEmailAddressChangeAction: controller.onEmailAddressFocusChange, - onShowFullListEmailAddressAction: controller.showFullEmailAddress, - onAddEmailAddressTypeAction: controller.addEmailAddressType, - onUpdateListEmailAddressAction: controller.updateListEmailAddress, - onSuggestionEmailAddress: controller.getAutoCompleteSuggestion, - onFocusNextAddressAction: controller.handleFocusNextAddressAction, - onRemoveDraggableEmailAddressAction: controller.removeDraggableEmailAddress, - onEditRecipientAction: controller.onEditRecipient, - onClearFocusAction: controller.onClearFocusAction, - ), + if (controller.recipientsCollapsedState.value == PrefixRecipientState.enabled) + RecipientsCollapsedComposerWidget( + listEmailAddress: controller.allListEmailAddress, + margin: ComposerStyle.desktopRecipientMargin, + onShowAllRecipientsAction: controller.showFullRecipients, + ), + if (controller.toRecipientState.value == PrefixRecipientState.enabled) + _buildRecipientComposerWidget( + composerId: composerId, + prefix: PrefixEmailAddress.to, + controller: controller, + maxWidth: constraints.maxWidth, + listEmailAddress: controller.listToEmailAddress, + expandMode: controller.toAddressExpandMode.value, + textController: controller.toEmailAddressController, + focusNode: controller.toAddressFocusNode, + focusNodeKeyboard: controller.toAddressFocusNodeKeyboard, + keyTagEditor: controller.keyToEmailTagEditor, + nextFocusNode: controller.getNextFocusOfToEmailAddress(), + ), if (controller.ccRecipientState.value == PrefixRecipientState.enabled) - RecipientComposerWidget( + _buildRecipientComposerWidget( composerId: composerId, prefix: PrefixEmailAddress.cc, - listEmailAddress: controller.listCcEmailAddress, - imagePaths: controller.imagePaths, + controller: controller, maxWidth: constraints.maxWidth, - minInputLengthAutocomplete: controller.minInputLengthAutocomplete, + listEmailAddress: controller.listCcEmailAddress, expandMode: controller.ccAddressExpandMode.value, - controller: controller.ccEmailAddressController, + textController: controller.ccEmailAddressController, focusNode: controller.ccAddressFocusNode, focusNodeKeyboard: controller.ccAddressFocusNodeKeyboard, keyTagEditor: controller.keyCcEmailTagEditor, - isInitial: controller.isInitialRecipient.value, nextFocusNode: controller.getNextFocusOfCcEmailAddress(), - padding: ComposerStyle.tabletRecipientPadding, - margin: ComposerStyle.tabletRecipientMargin, - onFocusEmailAddressChangeAction: controller.onEmailAddressFocusChange, - onShowFullListEmailAddressAction: controller.showFullEmailAddress, - onDeleteEmailAddressTypeAction: controller.deleteEmailAddressType, - onUpdateListEmailAddressAction: controller.updateListEmailAddress, - onSuggestionEmailAddress: controller.getAutoCompleteSuggestion, - onFocusNextAddressAction: controller.handleFocusNextAddressAction, - onRemoveDraggableEmailAddressAction: controller.removeDraggableEmailAddress, - onEditRecipientAction: controller.onEditRecipient, - onClearFocusAction: controller.onClearFocusAction, ), if (controller.bccRecipientState.value == PrefixRecipientState.enabled) - RecipientComposerWidget( + _buildRecipientComposerWidget( composerId: composerId, prefix: PrefixEmailAddress.bcc, - listEmailAddress: controller.listBccEmailAddress, - imagePaths: controller.imagePaths, + controller: controller, maxWidth: constraints.maxWidth, - minInputLengthAutocomplete: controller.minInputLengthAutocomplete, + listEmailAddress: controller.listBccEmailAddress, expandMode: controller.bccAddressExpandMode.value, - controller: controller.bccEmailAddressController, + textController: controller.bccEmailAddressController, focusNode: controller.bccAddressFocusNode, focusNodeKeyboard: controller.bccAddressFocusNodeKeyboard, keyTagEditor: controller.keyBccEmailTagEditor, - isInitial: controller.isInitialRecipient.value, nextFocusNode: controller.getNextFocusOfBccEmailAddress(), - padding: ComposerStyle.tabletRecipientPadding, - margin: ComposerStyle.tabletRecipientMargin, - onFocusEmailAddressChangeAction: controller.onEmailAddressFocusChange, - onShowFullListEmailAddressAction: controller.showFullEmailAddress, - onDeleteEmailAddressTypeAction: controller.deleteEmailAddressType, - onUpdateListEmailAddressAction: controller.updateListEmailAddress, - onSuggestionEmailAddress: controller.getAutoCompleteSuggestion, - onFocusNextAddressAction: controller.handleFocusNextAddressAction, - onRemoveDraggableEmailAddressAction: controller.removeDraggableEmailAddress, - onEditRecipientAction: controller.onEditRecipient, - onClearFocusAction: controller.onClearFocusAction, ), if (controller.replyToRecipientState.value == PrefixRecipientState.enabled) - RecipientComposerWidget( + _buildRecipientComposerWidget( composerId: composerId, prefix: PrefixEmailAddress.replyTo, - listEmailAddress: controller.listReplyToEmailAddress, - imagePaths: controller.imagePaths, + controller: controller, maxWidth: constraints.maxWidth, + listEmailAddress: controller.listReplyToEmailAddress, expandMode: controller.replyToAddressExpandMode.value, - controller: controller.replyToEmailAddressController, + textController: controller.replyToEmailAddressController, focusNode: controller.replyToAddressFocusNode, focusNodeKeyboard: controller.replyToAddressFocusNodeKeyboard, keyTagEditor: controller.keyReplyToEmailTagEditor, - isInitial: controller.isInitialRecipient.value, nextFocusNode: controller.subjectEmailInputFocusNode, - padding: ComposerStyle.tabletRecipientPadding, - margin: ComposerStyle.tabletRecipientMargin, - onFocusEmailAddressChangeAction: controller.onEmailAddressFocusChange, - onShowFullListEmailAddressAction: controller.showFullEmailAddress, - onDeleteEmailAddressTypeAction: controller.deleteEmailAddressType, - onUpdateListEmailAddressAction: controller.updateListEmailAddress, - onSuggestionEmailAddress: controller.getAutoCompleteSuggestion, - onFocusNextAddressAction: controller.handleFocusNextAddressAction, - onRemoveDraggableEmailAddressAction: controller.removeDraggableEmailAddress, - onEditRecipientAction: controller.onEditRecipient, - onClearFocusAction: controller.onClearFocusAction, ), ], )), @@ -995,4 +859,58 @@ class ComposerView extends GetWidget { ) ); } + + Widget _buildRecipientComposerWidget({ + required String? composerId, + required PrefixEmailAddress prefix, + required ComposerController controller, + required double maxWidth, + required List listEmailAddress, + required ExpandMode expandMode, + required TextEditingController textController, + required FocusNode? focusNode, + required FocusNode? focusNodeKeyboard, + required GlobalKey keyTagEditor, + required FocusNode? nextFocusNode, + bool isMobile = false, + }) { + return RecipientComposerWidget( + composerId: composerId, + prefix: prefix, + prefixRootState: controller.prefixRootState, + fromState: controller.fromRecipientState.value, + toState: controller.toRecipientState.value, + ccState: controller.ccRecipientState.value, + bccState: controller.bccRecipientState.value, + replyToState: controller.replyToRecipientState.value, + listEmailAddress: listEmailAddress, + imagePaths: controller.imagePaths, + maxWidth: maxWidth, + minInputLengthAutocomplete: controller.minInputLengthAutocomplete, + expandMode: expandMode, + controller: textController, + focusNode: focusNode, + focusNodeKeyboard: focusNodeKeyboard, + keyTagEditor: keyTagEditor, + isInitial: controller.isInitialRecipient.value, + nextFocusNode: nextFocusNode, + padding: isMobile + ? ComposerStyle.mobileRecipientPadding + : ComposerStyle.desktopRecipientPadding, + margin: isMobile + ? ComposerStyle.mobileRecipientMargin + : ComposerStyle.desktopRecipientMargin, + onFocusEmailAddressChangeAction: controller.onEmailAddressFocusChange, + onShowFullListEmailAddressAction: controller.showFullEmailAddress, + onUpdateListEmailAddressAction: controller.updateListEmailAddress, + onSuggestionEmailAddress: controller.getAutoCompleteSuggestion, + onFocusNextAddressAction: controller.handleFocusNextAddressAction, + onRemoveDraggableEmailAddressAction: + controller.removeDraggableEmailAddress, + onEditRecipientAction: controller.onEditRecipient, + onClearFocusAction: controller.onClearFocusAction, + onAddEmailAddressTypeAction: controller.addEmailAddressType, + onDeleteEmailAddressTypeAction: controller.deleteEmailAddressType, + ); + } } \ No newline at end of file diff --git a/lib/features/composer/presentation/extensions/handle_recipients_collapsed_extensions.dart b/lib/features/composer/presentation/extensions/handle_recipients_collapsed_extensions.dart new file mode 100644 index 000000000..6b91b80dc --- /dev/null +++ b/lib/features/composer/presentation/extensions/handle_recipients_collapsed_extensions.dart @@ -0,0 +1,105 @@ +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/composer/presentation/composer_controller.dart'; +import 'package:tmail_ui_user/features/composer/presentation/model/prefix_recipient_state.dart'; +import 'package:tmail_ui_user/features/email/presentation/utils/email_utils.dart'; + +extension HandleRecipientsCollapsedExtensions on ComposerController { + List get allListEmailAddress => + listToEmailAddress + + listCcEmailAddress + + listBccEmailAddress + + listReplyToEmailAddress; + + List get listEmailAddressInvalid => allListEmailAddress + .where( + (emailAddress) => !EmailUtils.isEmailAddressValid( + emailAddress.emailAddress, + ), + ) + .toList(); + + bool get isRecipientsNotEmpty => listToEmailAddress.isNotEmpty || + listCcEmailAddress.isNotEmpty || + listBccEmailAddress.isNotEmpty || + listReplyToEmailAddress.isNotEmpty; + + void showFullRecipients() { + updatePrefixRootState(); + recipientsCollapsedState.value = PrefixRecipientState.disabled; + + if (listToEmailAddress.isNotEmpty) { + toRecipientState.value = PrefixRecipientState.enabled; + toAddressExpandMode.value = ExpandMode.EXPAND; + } + + if (listCcEmailAddress.isNotEmpty) { + ccRecipientState.value = PrefixRecipientState.enabled; + ccAddressExpandMode.value = ExpandMode.EXPAND; + } + + if (listBccEmailAddress.isNotEmpty) { + bccRecipientState.value = PrefixRecipientState.enabled; + bccAddressExpandMode.value = ExpandMode.EXPAND; + } + + if (listReplyToEmailAddress.isNotEmpty) { + replyToRecipientState.value = PrefixRecipientState.enabled; + replyToAddressExpandMode.value = ExpandMode.EXPAND; + } + + requestFocusRecipientInput(); + } + + void updatePrefixRootState() { + if (listToEmailAddress.isNotEmpty) { + prefixRootState = PrefixEmailAddress.to; + return; + } + + if (listCcEmailAddress.isNotEmpty) { + prefixRootState = PrefixEmailAddress.cc; + return; + } + + if (listBccEmailAddress.isNotEmpty) { + prefixRootState = PrefixEmailAddress.bcc; + return; + } + + if (listReplyToEmailAddress.isNotEmpty) { + prefixRootState = PrefixEmailAddress.replyTo; + } + } + + void requestFocusRecipientInput() { + if (listToEmailAddress.isNotEmpty) { + toAddressFocusNode?.requestFocus(); + return; + } + + if (listCcEmailAddress.isNotEmpty) { + ccAddressFocusNode?.requestFocus(); + return; + } + + if (listBccEmailAddress.isNotEmpty) { + bccAddressFocusNode?.requestFocus(); + return; + } + + if (listReplyToEmailAddress.isNotEmpty) { + replyToAddressFocusNode?.requestFocus(); + } + } + + void hideAllRecipients() { + toRecipientState.value = PrefixRecipientState.disabled; + ccRecipientState.value = PrefixRecipientState.disabled; + bccRecipientState.value = PrefixRecipientState.disabled; + replyToRecipientState.value = PrefixRecipientState.disabled; + recipientsCollapsedState.value = PrefixRecipientState.enabled; + } +} diff --git a/lib/features/composer/presentation/styles/recipient_tag_item_widget_style.dart b/lib/features/composer/presentation/styles/recipient_tag_item_widget_style.dart index cea194cf1..a5bf2c713 100644 --- a/lib/features/composer/presentation/styles/recipient_tag_item_widget_style.dart +++ b/lib/features/composer/presentation/styles/recipient_tag_item_widget_style.dart @@ -4,7 +4,6 @@ import 'package:flutter/material.dart'; class RecipientTagItemWidgetStyle { static const double radius = 10; static const double avatarIconSize = 20; - static const double avatarLabelFontSize = 12; static const EdgeInsetsGeometry padding = EdgeInsetsDirectional.only(start: 4); static const EdgeInsetsGeometry counterPadding = EdgeInsetsDirectional.symmetric(vertical: 5, horizontal: 8); @@ -13,9 +12,17 @@ class RecipientTagItemWidgetStyle { static const EdgeInsetsGeometry webMobileCounterMargin = EdgeInsetsDirectional.only(start: 8); static const EdgeInsetsGeometry webCounterMargin = EdgeInsetsDirectional.only(top: 8, start: 8); - static TextStyle labelTextStyle = ThemeUtils.defaultTextStyleInterFont.copyWith( + static TextStyle labelTextStyle = ThemeUtils.textStyleInter400.copyWith( color: Colors.black, fontSize: 17, - fontWeight: FontWeight.w400 + height: 1.0, + letterSpacing: -0.17, + ); + + static TextStyle avatarTextStyle = ThemeUtils.textStyleInter500().copyWith( + color: Colors.white, + fontSize: 14, + height: 19.2 / 14, + letterSpacing: 0.0, ); } \ No newline at end of file diff --git a/lib/features/composer/presentation/widgets/draggable_recipient_tag_widget.dart b/lib/features/composer/presentation/widgets/draggable_recipient_tag_widget.dart index a4573699e..6f96a9f6c 100644 --- a/lib/features/composer/presentation/widgets/draggable_recipient_tag_widget.dart +++ b/lib/features/composer/presentation/widgets/draggable_recipient_tag_widget.dart @@ -1,25 +1,21 @@ 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/avatar/gradient_circle_avatar_icon.dart'; import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; -import 'package:get/get.dart'; import 'package:jmap_dart_client/jmap/mail/email/email_address.dart'; import 'package:model/extensions/email_address_extension.dart'; -import 'package:tmail_ui_user/features/composer/presentation/styles/draggable_recipient_tag_widget_style.dart'; class DraggableRecipientTagWidget extends StatelessWidget { - final EmailAddress emailAddress; - final EdgeInsetsGeometry? padding; + final ImagePaths imagePaths; - final _imagePaths = Get.find(); - - DraggableRecipientTagWidget({ + const DraggableRecipientTagWidget({ super.key, required this.emailAddress, - this.padding, + required this.imagePaths, }); @override @@ -27,38 +23,54 @@ class DraggableRecipientTagWidget extends StatelessWidget { return MouseRegion( cursor: SystemMouseCursors.grab, child: Container( - decoration: const ShapeDecoration( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.all(Radius.circular(DraggableRecipientTagWidgetStyle.radius)), - ), - color: DraggableRecipientTagWidgetStyle.backgroundColor + constraints: const BoxConstraints(maxWidth: 267), + decoration: const BoxDecoration( + borderRadius: BorderRadius.all(Radius.circular(10)), + color: AppColor.primaryColor, ), - padding: padding ?? DraggableRecipientTagWidgetStyle.padding, + height: 32, + padding: const EdgeInsetsDirectional.only(start: 8, end: 4), child: Row( mainAxisSize: MainAxisSize.min, children: [ - if (emailAddress.emailAddress.isNotEmpty) - GradientCircleAvatarIcon( - colors: emailAddress.avatarColors, - label: emailAddress.emailAddress.firstCharacterToUpperCase, - labelFontSize: DraggableRecipientTagWidgetStyle.avatarLabelFontSize, - iconSize: DraggableRecipientTagWidgetStyle.avatarIconSize, + GradientCircleAvatarIcon( + colors: emailAddress.avatarColors, + label: emailAddress.asString().firstCharacterToUpperCase, + textStyle: ThemeUtils.textStyleInter500().copyWith( + color: Colors.white, + fontSize: 14, + height: 19.2 / 14, + letterSpacing: 0.0, ), - Padding( - padding: DraggableRecipientTagWidgetStyle.labelPadding, - child: DefaultTextStyle( - style: DraggableRecipientTagWidgetStyle.labelTextStyle, - child: Text(emailAddress.asString()), + iconSize: 20, + ), + const SizedBox(width: 4), + Flexible( + child: Text( + emailAddress.asString(), + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: ThemeUtils.textStyleInter400.copyWith( + color: Colors.white, + fontSize: 17, + height: 1.0, + letterSpacing: -0.17, + ), ), ), - SvgPicture.asset( - _imagePaths.icClose, - colorFilter: DraggableRecipientTagWidgetStyle.deleteIconColor.asFilter(), - fit: BoxFit.fill + const SizedBox(width: 4), + Padding( + padding: const EdgeInsets.all(4), + child: SvgPicture.asset( + imagePaths.icClose, + width: 20, + height: 20, + colorFilter: AppColor.m3SysOutline.asFilter(), + ), ) ], ), ), ); } -} \ No newline at end of file +} diff --git a/lib/features/composer/presentation/widgets/list_recipients_collapsed_widget.dart b/lib/features/composer/presentation/widgets/list_recipients_collapsed_widget.dart new file mode 100644 index 000000000..a52db4aa0 --- /dev/null +++ b/lib/features/composer/presentation/widgets/list_recipients_collapsed_widget.dart @@ -0,0 +1,103 @@ +import 'package:core/presentation/extensions/color_extension.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:tmail_ui_user/features/composer/presentation/styles/recipient_composer_widget_style.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/recipient_collapsed_item_widget.dart'; + +class RecipientsCollapsedComposerWidget extends StatelessWidget { + final List listEmailAddress; + final VoidCallback onShowAllRecipientsAction; + final EdgeInsetsGeometry? margin; + + const RecipientsCollapsedComposerWidget({ + super.key, + required this.listEmailAddress, + required this.onShowAllRecipientsAction, + this.margin, + }); + + @override + Widget build(BuildContext context) { + return LayoutBuilder( + builder: (context, constraints) { + double maxWidth = constraints.maxWidth; + double usedWidth = 0; + const double spacing = 8.0; + + List visibleChips = []; + int hiddenCount = 0; + + for (var emailAddress in listEmailAddress) { + final textWidth = + _estimateTextWidth(context, emailAddress.asString()) + 80; + if (usedWidth + textWidth > maxWidth - 60) { + hiddenCount = listEmailAddress.length - visibleChips.length; + break; + } + usedWidth += textWidth + spacing; + visibleChips.add( + Padding( + padding: const EdgeInsetsDirectional.only(end: spacing), + child: RecipientCollapsedItemWidget(emailAddress: emailAddress), + ), + ); + } + + if (hiddenCount > 0) { + final label = hiddenCount > 999 ? '999+' : '+$hiddenCount'; + visibleChips.add( + Container( + padding: const EdgeInsets.symmetric(horizontal: 8), + decoration: const BoxDecoration( + borderRadius: BorderRadius.all( + Radius.circular(RecipientTagItemWidgetStyle.radius), + ), + color: AppColor.grayBackgroundColor, + ), + height: 32, + alignment: Alignment.center, + child: Text( + label, + style: RecipientTagItemWidgetStyle.labelTextStyle, + ), + ), + ); + } + + return MouseRegion( + cursor: SystemMouseCursors.text, + child: GestureDetector( + onTap: onShowAllRecipientsAction, + child: Container( + margin: margin, + padding: const EdgeInsets.symmetric(vertical: 8), + decoration: const BoxDecoration( + border: Border( + bottom: BorderSide( + color: RecipientComposerWidgetStyle.borderColor, + width: 1, + ), + ), + ), + child: Row(children: visibleChips), + ), + ), + ); + }, + ); + } + + double _estimateTextWidth(BuildContext context, String text) { + final textPainter = TextPainter( + text: TextSpan( + text: text, + style: DefaultTextStyle.of(context).style, + ), + maxLines: 1, + textDirection: TextDirection.ltr, + )..layout(); + return textPainter.width; + } +} diff --git a/lib/features/composer/presentation/widgets/recipient_collapsed_item_widget.dart b/lib/features/composer/presentation/widgets/recipient_collapsed_item_widget.dart new file mode 100644 index 000000000..7bca714f2 --- /dev/null +++ b/lib/features/composer/presentation/widgets/recipient_collapsed_item_widget.dart @@ -0,0 +1,75 @@ +import 'package:core/presentation/extensions/color_extension.dart'; +import 'package:core/presentation/extensions/string_extension.dart'; +import 'package:core/presentation/views/avatar/gradient_circle_avatar_icon.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:tmail_ui_user/features/composer/presentation/styles/recipient_tag_item_widget_style.dart'; +import 'package:tmail_ui_user/features/email/presentation/utils/email_utils.dart'; + +class RecipientCollapsedItemWidget extends StatelessWidget { + final EmailAddress emailAddress; + + const RecipientCollapsedItemWidget({ + super.key, + required this.emailAddress, + }); + + @override + Widget build(BuildContext context) { + return Container( + constraints: const BoxConstraints(maxWidth: 267), + decoration: BoxDecoration( + borderRadius: const BorderRadius.all( + Radius.circular(RecipientTagItemWidgetStyle.radius), + ), + border: _getTagBorder(), + color: _getTagBackgroundColor(), + ), + height: 32, + padding: const EdgeInsets.symmetric(horizontal: 8), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + GradientCircleAvatarIcon( + colors: emailAddress.avatarColors, + label: emailAddress.asString().firstCharacterToUpperCase, + textStyle: RecipientTagItemWidgetStyle.avatarTextStyle, + iconSize: RecipientTagItemWidgetStyle.avatarIconSize, + ), + const SizedBox(width: 4), + Flexible( + child: Text( + emailAddress.asString(), + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: RecipientTagItemWidgetStyle.labelTextStyle, + ), + ), + ], + ), + ); + } + + Color _getTagBackgroundColor() { + if (EmailUtils.isEmailAddressValid(emailAddress.emailAddress)) { + return AppColor.grayBackgroundColor; + } else { + return Colors.white; + } + } + + Border _getTagBorder() { + if (EmailUtils.isEmailAddressValid(emailAddress.emailAddress)) { + return Border.all( + width: 1, + color: AppColor.grayBackgroundColor, + ); + } else { + return Border.all( + width: 1, + color: AppColor.colorBorderEmailAddressInvalid, + ); + } + } +} diff --git a/lib/features/composer/presentation/widgets/recipient_composer_widget.dart b/lib/features/composer/presentation/widgets/recipient_composer_widget.dart index 8c422a167..8f7aad219 100644 --- a/lib/features/composer/presentation/widgets/recipient_composer_widget.dart +++ b/lib/features/composer/presentation/widgets/recipient_composer_widget.dart @@ -56,9 +56,11 @@ class RecipientComposerWidget extends StatefulWidget { final double maxWidth; final ExpandMode expandMode; final PrefixRecipientState fromState; + final PrefixRecipientState toState; final PrefixRecipientState ccState; final PrefixRecipientState bccState; final PrefixRecipientState replyToState; + final PrefixEmailAddress prefixRootState; final bool? isInitial; final FocusNode? focusNode; final FocusNode? focusNodeKeyboard; @@ -90,10 +92,12 @@ class RecipientComposerWidget extends StatefulWidget { required this.maxWidth, this.minInputLengthAutocomplete = AppConfig.defaultMinInputLengthAutocomplete, @visibleForTesting this.isTestingForWeb = false, + this.toState = PrefixRecipientState.disabled, this.ccState = PrefixRecipientState.disabled, this.bccState = PrefixRecipientState.disabled, this.replyToState = PrefixRecipientState.disabled, this.fromState = PrefixRecipientState.disabled, + this.prefixRootState = PrefixEmailAddress.to, this.isInitial, this.controller, this.focusNode, @@ -360,7 +364,7 @@ class _RecipientComposerWidgetState extends State { ) ), const SizedBox(width: RecipientComposerWidgetStyle.space), - if (widget.prefix == PrefixEmailAddress.to) + if (widget.prefix == widget.prefixRootState) if (PlatformInfo.isWeb || widget.isTestingForWeb) ...[ if (widget.fromState == PrefixRecipientState.disabled) @@ -373,6 +377,16 @@ class _RecipientComposerWidgetState extends State { margin: RecipientComposerWidgetStyle.recipientMargin, onTapActionCallback: () => widget.onAddEmailAddressTypeAction?.call(PrefixEmailAddress.from), ), + if (widget.toState == PrefixRecipientState.disabled) + TMailButtonWidget.fromText( + key: Key('prefix_${widget.prefix.name}_recipient_to_button'), + text: AppLocalizations.of(context).to_email_address_prefix, + textStyle: RecipientComposerWidgetStyle.prefixButtonTextStyle, + backgroundColor: Colors.transparent, + padding: RecipientComposerWidgetStyle.prefixButtonPadding, + margin: RecipientComposerWidgetStyle.recipientMargin, + onTapActionCallback: () => widget.onAddEmailAddressTypeAction?.call(PrefixEmailAddress.to), + ), if (widget.ccState == PrefixRecipientState.disabled) TMailButtonWidget.fromText( key: Key('prefix_${widget.prefix.name}_recipient_cc_button'), @@ -404,7 +418,7 @@ class _RecipientComposerWidgetState extends State { onTapActionCallback: () => widget.onAddEmailAddressTypeAction?.call(PrefixEmailAddress.replyTo), ), ] - else if (PlatformInfo.isMobile) + else TMailButtonWidget.fromIcon( key: Key('prefix_${widget.prefix.name}_recipient_expand_button'), icon: _isAllRecipientInputEnabled 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 71cb35853..91df8e9a9 100644 --- a/lib/features/composer/presentation/widgets/recipient_tag_item_widget.dart +++ b/lib/features/composer/presentation/widgets/recipient_tag_item_widget.dart @@ -3,10 +3,8 @@ import 'package:core/presentation/extensions/string_extension.dart'; import 'package:core/presentation/resources/image_paths.dart'; import 'package:core/presentation/views/avatar/gradient_circle_avatar_icon.dart'; import 'package:core/presentation/views/button/tmail_button_widget.dart'; -import 'package:core/utils/direction_utils.dart'; import 'package:core/utils/platform_info.dart'; import 'package:flutter/material.dart'; -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'; @@ -95,40 +93,51 @@ class RecipientTagItemWidget extends StatelessWidget { onCloseAction: onClose, ), onClearFocusAction: onClearFocusAction, - child: Chip( - labelPadding: EdgeInsetsDirectional.symmetric( - horizontal: 4, - vertical: DirectionUtils.isDirectionRTLByHasAnyRtl(currentEmailAddress.asString()) ? 0 : 2 + child: Container( + constraints: const BoxConstraints(maxWidth: 267), + decoration: BoxDecoration( + borderRadius: const BorderRadius.all( + Radius.circular(RecipientTagItemWidgetStyle.radius), + ), + border: _getTagBorder(), + color: _getTagBackgroundColor(), ), - padding: EdgeInsets.zero, - label: Text( - key: Key('label_recipient_tag_item_${prefix.name}_$index'), - currentEmailAddress.asString(), - maxLines: 1, - overflow: TextOverflow.ellipsis, - softWrap: true, - ), - deleteIcon: SvgPicture.asset( - imagePaths.icClose, - key: Key('delete_icon_recipient_tag_item_${prefix.name}_$index'), - fit: BoxFit.fill - ), - labelStyle: RecipientTagItemWidgetStyle.labelTextStyle, - backgroundColor: _getTagBackgroundColor(), - side: _getTagBorderSide(), - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.all(Radius.circular(RecipientTagItemWidgetStyle.radius)), - ), - avatar: currentEmailAddress.displayName.isNotEmpty - ? GradientCircleAvatarIcon( + height: 32, + padding: const EdgeInsetsDirectional.only(start: 8, end: 4), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + GradientCircleAvatarIcon( key: Key('avatar_icon_recipient_tag_item_${prefix.name}_$index'), colors: currentEmailAddress.avatarColors, - label: currentEmailAddress.displayName.firstLetterToUpperCase, - labelFontSize: RecipientTagItemWidgetStyle.avatarLabelFontSize, + label: currentEmailAddress.asString().firstCharacterToUpperCase, + textStyle: RecipientTagItemWidgetStyle.avatarTextStyle, iconSize: RecipientTagItemWidgetStyle.avatarIconSize, + ), + const SizedBox(width: 4), + Flexible( + child: Text( + key: Key('label_recipient_tag_item_${prefix.name}_$index'), + currentEmailAddress.asString(), + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: RecipientTagItemWidgetStyle.labelTextStyle, + ), + ), + const SizedBox(width: 4), + TMailButtonWidget.fromIcon( + key: Key('delete_icon_recipient_tag_item_${prefix.name}_$index'), + icon: imagePaths.icClose, + iconSize: 20, + iconColor: AppColor.m3SysOutline, + padding: const EdgeInsets.all(4), + borderRadius: 100, + backgroundColor: Colors.transparent, + onTapActionCallback: () => + onDeleteTagAction?.call(currentEmailAddress), ) - : null, - onDeleted: () => onDeleteTagAction?.call(currentEmailAddress), + ], + ), ), ); @@ -139,12 +148,15 @@ class RecipientTagItemWidget extends StatelessWidget { filterField: prefix.filterField, composerId: composerId, ), - feedback: DraggableRecipientTagWidget(emailAddress: currentEmailAddress), - childWhenDragging: DraggableRecipientTagWidget(emailAddress: currentEmailAddress), - child: MouseRegion( - cursor: SystemMouseCursors.grab, - child: tagWidget, + feedback: DraggableRecipientTagWidget( + imagePaths: imagePaths, + emailAddress: currentEmailAddress, ), + childWhenDragging: DraggableRecipientTagWidget( + imagePaths: imagePaths, + emailAddress: currentEmailAddress, + ), + child: tagWidget, ); } @@ -202,15 +214,21 @@ class RecipientTagItemWidget extends StatelessWidget { } } - BorderSide _getTagBorderSide() { + Border _getTagBorder() { if (isTagFocused) { - return const BorderSide(width: 1, color: AppColor.primaryColor); - } else if (EmailUtils.isEmailAddressValid(currentEmailAddress.emailAddress)) { - return const BorderSide(width: 1, color: AppColor.colorEmailAddressTag); - } else { - return const BorderSide( + return Border.all( width: 1, - color: AppColor.colorBorderEmailAddressInvalid + color: AppColor.primaryColor, + ); + } else if (EmailUtils.isEmailAddressValid(currentEmailAddress.emailAddress)) { + return Border.all( + width: 1, + color: AppColor.grayBackgroundColor, + ); + } else { + return Border.all( + width: 1, + color: AppColor.colorBorderEmailAddressInvalid, ); } }