From 0c372d0465cbce81feae83fe7fe3e8647968586a Mon Sep 17 00:00:00 2001 From: DatDang Date: Thu, 17 Jul 2025 15:50:05 +0700 Subject: [PATCH] TF-3881 Thread Detail Reply action only for collapsed email TF-3881 Thread Detail Hide sender email on collapsed or mobile view TF-3881 Thread Detail Show attachment icon on collapsed TF-3881 Thread Detail Update date format of received email time Thread Detail update context menu --- .../domain/extensions/datetime_extension.dart | 6 +- .../email/presentation/email_view.dart | 14 +-- .../email_action_reactor.dart | 86 +++++++------------ .../widgets/email_view_app_bar_widget.dart | 5 ++ ...formation_sender_and_receiver_builder.dart | 54 +++++++++++- .../widgets/received_time_builder.dart | 20 ++++- .../get_thread_details_email_views.dart | 3 + .../presentation/thread_detail_view.dart | 3 + lib/main/localizations/app_localizations.dart | 9 ++ 9 files changed, 132 insertions(+), 68 deletions(-) diff --git a/core/lib/domain/extensions/datetime_extension.dart b/core/lib/domain/extensions/datetime_extension.dart index 4befa02ca..9dd32888a 100644 --- a/core/lib/domain/extensions/datetime_extension.dart +++ b/core/lib/domain/extensions/datetime_extension.dart @@ -50,11 +50,7 @@ extension DateTimeNullableExtension on DateTime? { } String toPatternForEmailView() { - if (this?.isThisYear() == true) { - return 'dd.MM, HH:mm'; - } else { - return 'dd.MM.yyyy, HH:mm'; - } + return 'dd MMM yyyy, HH:mm a'; } String toPatternForPrinting(String locale) { diff --git a/lib/features/email/presentation/email_view.dart b/lib/features/email/presentation/email_view.dart index 4a775c379..a234d48c2 100644 --- a/lib/features/email/presentation/email_view.dart +++ b/lib/features/email/presentation/email_view.dart @@ -32,6 +32,7 @@ import 'package:tmail_ui_user/features/email/presentation/widgets/email_view_loa import 'package:tmail_ui_user/features/email/presentation/widgets/information_sender_and_receiver_builder.dart'; import 'package:tmail_ui_user/features/email/presentation/widgets/mail_unsubscribed_banner.dart'; import 'package:tmail_ui_user/features/email/presentation/widgets/view_entire_message_with_message_clipped_widget.dart'; +import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/extensions/handle_open_context_menu_extension.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/extensions/verify_display_overlay_view_on_iframe_extension.dart'; import 'package:tmail_ui_user/features/manage_account/presentation/extensions/vacation_response_extension.dart'; import 'package:tmail_ui_user/features/manage_account/presentation/vacation/widgets/vacation_notification_message_widget.dart'; @@ -98,6 +99,8 @@ class EmailView extends GetWidget { handleEmailAction: (email, action) => controller.handleEmailAction(context, email, action), additionalActions: [], emailIsRead: presentationEmail.hasRead, + openBottomSheetContextMenu: controller.mailboxDashBoardController.openBottomSheetContextMenu, + openPopupMenu: controller.mailboxDashBoardController.openPopupMenu, ), supportBackAction: !isInsideThreadDetailView, appBarDecoration: isInsideThreadDetailView @@ -303,6 +306,8 @@ class EmailView extends GetWidget { ], ], emailIsRead: presentationEmail.hasRead, + openBottomSheetContextMenu: controller.mailboxDashBoardController.openBottomSheetContextMenu, + openPopupMenu: controller.mailboxDashBoardController.openPopupMenu, ), onToggleThreadDetailCollapseExpand: onToggleThreadDetailCollapseExpand, onTapAvatarActionClick: onToggleThreadDetailCollapseExpand, @@ -416,11 +421,10 @@ class EmailView extends GetWidget { enableQuoteToggle: isInsideThreadDetailView, ), if (controller.mailboxDashBoardController.isDisplayedOverlayViewOnIFrame) - PointerInterceptor( - child: SizedBox( - width: constraints.maxWidth, - height: constraints.maxHeight, - ) + Positioned.fill( + child: PointerInterceptor( + child: const SizedBox.expand(), + ), ), ], ); diff --git a/lib/features/email/presentation/utils/email_action_reactor/email_action_reactor.dart b/lib/features/email/presentation/utils/email_action_reactor/email_action_reactor.dart index db1e47456..c8831d5b4 100644 --- a/lib/features/email/presentation/utils/email_action_reactor/email_action_reactor.dart +++ b/lib/features/email/presentation/utils/email_action_reactor/email_action_reactor.dart @@ -18,7 +18,6 @@ import 'package:dartz/dartz.dart'; import 'package:dio/dio.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:flutter_svg/flutter_svg.dart'; import 'package:get/get.dart'; import 'package:jmap_dart_client/jmap/account_id.dart'; import 'package:jmap_dart_client/jmap/core/session/session.dart'; @@ -39,9 +38,8 @@ import 'package:model/extensions/session_extension.dart'; import 'package:model/mailbox/presentation_mailbox.dart'; import 'package:pointer_interceptor/pointer_interceptor.dart'; import 'package:tmail_ui_user/features/base/mixin/message_dialog_action_mixin.dart'; -import 'package:tmail_ui_user/features/base/mixin/popup_context_menu_action_mixin.dart'; +import 'package:tmail_ui_user/features/base/widget/context_menu/context_menu_item_action.dart'; import 'package:tmail_ui_user/features/base/widget/popup_menu/popup_menu_item_action_widget.dart'; -import 'package:tmail_ui_user/features/composer/presentation/extensions/email_action_type_extension.dart'; import 'package:tmail_ui_user/features/destination_picker/presentation/model/destination_picker_arguments.dart'; import 'package:tmail_ui_user/features/email/domain/exceptions/email_exceptions.dart'; import 'package:tmail_ui_user/features/email/domain/model/email_print.dart'; @@ -56,11 +54,11 @@ import 'package:tmail_ui_user/features/email/domain/usecases/get_email_content_i import 'package:tmail_ui_user/features/email/domain/usecases/mark_as_email_read_interactor.dart'; import 'package:tmail_ui_user/features/email/domain/usecases/mark_as_star_email_interactor.dart'; import 'package:tmail_ui_user/features/email/domain/usecases/print_email_interactor.dart'; +import 'package:tmail_ui_user/features/email/presentation/model/context_item_email_action.dart'; import 'package:tmail_ui_user/features/email/presentation/model/email_loaded.dart'; import 'package:tmail_ui_user/features/email/presentation/model/email_unsubscribe.dart'; import 'package:tmail_ui_user/features/email/presentation/model/popup_menu_item_email_action.dart'; import 'package:tmail_ui_user/features/email/presentation/utils/email_utils.dart'; -import 'package:tmail_ui_user/features/email/presentation/widgets/email_action_cupertino_action_sheet_action_builder.dart'; import 'package:tmail_ui_user/features/email/presentation/widgets/email_address_bottom_sheet_builder.dart'; import 'package:tmail_ui_user/features/email/presentation/widgets/email_address_dialog_builder.dart'; import 'package:tmail_ui_user/features/mailbox/presentation/model/mailbox_actions.dart'; @@ -77,7 +75,20 @@ import 'package:tmail_ui_user/main/routes/route_utils.dart'; import 'package:tmail_ui_user/main/utils/app_utils.dart'; import 'package:uuid/uuid.dart'; -class EmailActionReactor with MessageDialogActionMixin, PopupContextMenuActionMixin { +typedef OpenBottomSheetContextMenuAction = Future Function({ + required BuildContext context, + required List itemActions, + required OnContextMenuActionClick onContextMenuActionClick, + Key? key, +}); + +typedef OpenPopUpContextMenuAction = Future Function( + BuildContext context, + RelativeRect position, + List popupMenuItems, +); + +class EmailActionReactor with MessageDialogActionMixin { const EmailActionReactor( this._markAsEmailReadInteractor, this._markAsStarEmailInteractor, @@ -523,6 +534,8 @@ class EmailActionReactor with MessageDialogActionMixin, PopupContextMenuActionMi ) handleEmailAction, required List additionalActions, required bool emailIsRead, + required OpenBottomSheetContextMenuAction openBottomSheetContextMenu, + required OpenPopUpContextMenuAction openPopupMenu, }) { if (currentContext == null) return; @@ -572,19 +585,23 @@ class EmailActionReactor with MessageDialogActionMixin, PopupContextMenuActionMi ]; if (position == null) { - openContextMenuAction( - currentContext!, - _emailActionMoreActionTile( - currentContext!, - presentationEmail, - moreActions, - responsiveUtils, - imagePaths, - handleEmailAction: handleEmailAction, - ) + openBottomSheetContextMenu( + context: currentContext!, + itemActions: moreActions + .map( + (action) => ContextItemEmailAction( + action, + AppLocalizations.of(currentContext!), + imagePaths, + ), + ) + .toList(), + onContextMenuActionClick: (action) { + handleEmailAction(presentationEmail, action.action); + }, ); } else { - openPopupMenuAction( + openPopupMenu( currentContext!, position, _popupMenuEmailActionTile( @@ -609,43 +626,6 @@ class EmailActionReactor with MessageDialogActionMixin, PopupContextMenuActionMi ?? false; } - List _emailActionMoreActionTile( - BuildContext context, - PresentationEmail presentationEmail, - List actionTypes, - ResponsiveUtils responsiveUtils, - ImagePaths imagePaths, { - required void Function( - PresentationEmail presentationEmail, - EmailActionType action, - ) handleEmailAction - }) { - return actionTypes.map((action) { - return (EmailActionCupertinoActionSheetActionBuilder( - Key('${action.name}_action'), - SvgPicture.asset( - action.getIcon(imagePaths), - width: 24, - height: 24, - fit: BoxFit.fill, - colorFilter: AppColor.colorTextButton.asFilter() - ), - action.getTitle(AppLocalizations.of(context)), - presentationEmail, - iconLeftPadding: responsiveUtils.isScreenWithShortestSide(context) - ? const EdgeInsetsDirectional.only(start: 12, end: 16) - : const EdgeInsetsDirectional.only(end: 12), - iconRightPadding: responsiveUtils.isScreenWithShortestSide(context) - ? const EdgeInsetsDirectional.only(end: 12) - : EdgeInsets.zero - ) - ..onActionClick((presentationEmail) { - popBack(); - handleEmailAction(presentationEmail, action); - })).build(); - }).toList(); - } - List _popupMenuEmailActionTile( BuildContext context, PresentationEmail presentationEmail, diff --git a/lib/features/email/presentation/widgets/email_view_app_bar_widget.dart b/lib/features/email/presentation/widgets/email_view_app_bar_widget.dart index cf1b5eac3..facf928d8 100644 --- a/lib/features/email/presentation/widgets/email_view_app_bar_widget.dart +++ b/lib/features/email/presentation/widgets/email_view_app_bar_widget.dart @@ -5,6 +5,7 @@ import 'package:core/utils/platform_info.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:model/email/email_action_type.dart'; +import 'package:model/email/email_in_thread_status.dart'; import 'package:model/email/presentation_email.dart'; import 'package:model/extensions/presentation_mailbox_extension.dart'; import 'package:model/mailbox/presentation_mailbox.dart'; @@ -258,6 +259,10 @@ class EmailViewAppBarWidget extends StatelessWidget { ]; } + if (presentationEmail.emailInThreadStatus == EmailInThreadStatus.collapsed) { + return [_getReplyButton(appLocalizations)]; + } + return [ if (!isOnlyEmailInThread) _getReplyButton(appLocalizations), diff --git a/lib/features/email/presentation/widgets/information_sender_and_receiver_builder.dart b/lib/features/email/presentation/widgets/information_sender_and_receiver_builder.dart index 0710c3bbd..73d93dc9c 100644 --- a/lib/features/email/presentation/widgets/information_sender_and_receiver_builder.dart +++ b/lib/features/email/presentation/widgets/information_sender_and_receiver_builder.dart @@ -9,6 +9,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:model/email/email_action_type.dart'; +import 'package:model/email/email_in_thread_status.dart'; import 'package:model/email/presentation_email.dart'; import 'package:model/extensions/presentation_email_extension.dart'; import 'package:model/mailbox/presentation_mailbox.dart'; @@ -108,6 +109,7 @@ class InformationSenderAndReceiverBuilder extends StatelessWidget { child: EmailSenderBuilder( emailAddress: emailSelected.from!.first, openEmailAddressDetailAction: openEmailAddressDetailAction, + showSenderEmail: _showSenderEmail(responsiveUtils.isMobile(context)), ), ), if (sMimeStatus != null && sMimeStatus != SMimeSignatureStatus.notSigned) @@ -135,10 +137,21 @@ class InformationSenderAndReceiverBuilder extends StatelessWidget { backgroundColor: Colors.transparent, onTapActionCallback: () => onEmailActionClick?.call(emailSelected, EmailActionType.unsubscribe), ), + if (_showAttachmentIcon() && !responsiveUtils.isMobile(context)) + Padding( + padding: const EdgeInsetsDirectional.only(start: 16), + child: SvgPicture.asset( + imagePaths.icAttachment, + colorFilter: AppColor.colorAttachmentIcon.asFilter(), + width: 20, + height: 20, + ), + ), if (isInsideThreadDetailView && !responsiveUtils.isMobile(context)) ReceivedTimeBuilder( emailSelected: emailSelected, - padding: const EdgeInsetsDirectional.only(start: 16, top: 2), + padding: const EdgeInsetsDirectional.only(start: 8, top: 2), + showDaysAgo: _showDaysAgo(responsiveUtils.isMobile(context)), ), if (showUnreadVisualization && !emailSelected.hasRead && @@ -160,6 +173,7 @@ class InformationSenderAndReceiverBuilder extends StatelessWidget { ReceivedTimeBuilder( emailSelected: emailSelected, padding: const EdgeInsetsDirectional.only(start: 16, top: 2), + showDaysAgo: _showDaysAgo(responsiveUtils.isMobile(context)), ), if (isInsideThreadDetailView) SizedBox( @@ -189,9 +203,24 @@ class InformationSenderAndReceiverBuilder extends StatelessWidget { ], ), if (responsiveUtils.isMobile(context) && isInsideThreadDetailView) - ReceivedTimeBuilder( - emailSelected: emailSelected, - padding: const EdgeInsetsDirectional.symmetric(vertical: 5), + Row( + children: [ + if (_showAttachmentIcon()) + Padding( + padding: const EdgeInsetsDirectional.only(end: 8), + child: SvgPicture.asset( + imagePaths.icAttachment, + colorFilter: AppColor.colorAttachmentIcon.asFilter(), + width: 20, + height: 20, + ), + ), + ReceivedTimeBuilder( + emailSelected: emailSelected, + padding: const EdgeInsetsDirectional.symmetric(vertical: 5), + showDaysAgo: _showDaysAgo(responsiveUtils.isMobile(context)), + ), + ], ), if (emailSelected.countRecipients > 0 && showRecipients) EmailReceiverWidget( @@ -208,4 +237,21 @@ class InformationSenderAndReceiverBuilder extends StatelessWidget { ), ); } + + bool _showSenderEmail(bool isResponsiveMobile) { + return emailSelected.emailInThreadStatus == EmailInThreadStatus.expanded && + !isResponsiveMobile; + } + + bool _showAttachmentIcon() { + return isInsideThreadDetailView && + emailSelected.hasAttachment == true && + emailSelected.emailInThreadStatus == EmailInThreadStatus.collapsed; + } + + bool _showDaysAgo(bool isResponsiveMobile) { + return isInsideThreadDetailView && + emailSelected.emailInThreadStatus == EmailInThreadStatus.collapsed && + !isResponsiveMobile; + } } \ No newline at end of file diff --git a/lib/features/email/presentation/widgets/received_time_builder.dart b/lib/features/email/presentation/widgets/received_time_builder.dart index b6ab585ac..ca78edcf0 100644 --- a/lib/features/email/presentation/widgets/received_time_builder.dart +++ b/lib/features/email/presentation/widgets/received_time_builder.dart @@ -4,29 +4,36 @@ import 'package:core/presentation/extensions/color_extension.dart'; import 'package:core/presentation/utils/style_utils.dart'; import 'package:core/presentation/utils/theme_utils.dart'; import 'package:flutter/material.dart'; +import 'package:jmap_dart_client/jmap/core/utc_date.dart'; import 'package:model/email/presentation_email.dart'; import 'package:model/extensions/presentation_email_extension.dart'; +import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; class ReceivedTimeBuilder extends StatelessWidget { final PresentationEmail emailSelected; final EdgeInsetsGeometry? padding; + final bool showDaysAgo; const ReceivedTimeBuilder({ Key? key, required this.emailSelected, this.padding, + this.showDaysAgo = false, }) : super(key: key); @override Widget build(BuildContext context) { + final appLocalizations = AppLocalizations.of(context); + final daysPast = getDaysPast(emailSelected.receivedAt, appLocalizations); + return Padding( padding: padding ?? const EdgeInsetsDirectional.only(start: 16), child: Text( emailSelected.getReceivedAt( Localizations.localeOf(context).toLanguageTag(), pattern: emailSelected.receivedAt?.value.toLocal().toPatternForEmailView() - ), + ).toLowerCase() + daysPast, maxLines: 1, overflow: CommonTextStyle.defaultTextOverFlow, softWrap: CommonTextStyle.defaultSoftWrap, @@ -37,4 +44,15 @@ class ReceivedTimeBuilder extends StatelessWidget { ), ); } + + String getDaysPast(UTCDate? utcDate, AppLocalizations appLocalizations) { + if (!showDaysAgo || utcDate == null) return ''; + + final from = utcDate.value; + final to = DateTime.now(); + if (from.isAfter(to)) return ''; + + final days = to.difference(from).inDays; + return appLocalizations.daysAgo(days); + } } \ No newline at end of file diff --git a/lib/features/thread_detail/presentation/extension/get_thread_details_email_views.dart b/lib/features/thread_detail/presentation/extension/get_thread_details_email_views.dart index 94f8369db..903cd8c33 100644 --- a/lib/features/thread_detail/presentation/extension/get_thread_details_email_views.dart +++ b/lib/features/thread_detail/presentation/extension/get_thread_details_email_views.dart @@ -3,6 +3,7 @@ import 'package:model/email/email_action_type.dart'; import 'package:model/extensions/presentation_email_extension.dart'; import 'package:tmail_ui_user/features/email/presentation/email_view.dart'; import 'package:model/email/email_in_thread_status.dart'; +import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/extensions/handle_open_context_menu_extension.dart'; import 'package:tmail_ui_user/features/thread_detail/presentation/extension/get_thread_detail_email_mailbox_contains.dart'; import 'package:tmail_ui_user/features/thread_detail/presentation/extension/load_more_thread_detail_emails.dart'; import 'package:tmail_ui_user/features/thread_detail/presentation/extension/thread_detail_on_email_action_click.dart'; @@ -88,6 +89,8 @@ extension GetThreadDetailEmailViews on ThreadDetailController { ], ], emailIsRead: presentationEmail.hasRead, + openBottomSheetContextMenu: mailboxDashBoardController.openBottomSheetContextMenu, + openPopupMenu: mailboxDashBoardController.openPopupMenu, ), openEmailAddressDetailAction: (_, emailAddress) { openEmailAddressDetailAction(emailAddress); diff --git a/lib/features/thread_detail/presentation/thread_detail_view.dart b/lib/features/thread_detail/presentation/thread_detail_view.dart index 263aaba32..5d92c197d 100644 --- a/lib/features/thread_detail/presentation/thread_detail_view.dart +++ b/lib/features/thread_detail/presentation/thread_detail_view.dart @@ -13,6 +13,7 @@ import 'package:model/mailbox/presentation_mailbox.dart'; import 'package:tmail_ui_user/features/email/presentation/action/email_ui_action.dart'; import 'package:tmail_ui_user/features/email/presentation/styles/email_view_app_bar_widget_styles.dart'; import 'package:tmail_ui_user/features/email/presentation/widgets/email_view_bottom_bar_widget.dart'; +import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/extensions/handle_open_context_menu_extension.dart'; import 'package:tmail_ui_user/features/thread_detail/domain/state/get_thread_by_id_state.dart'; import 'package:tmail_ui_user/features/thread_detail/presentation/extension/close_thread_detail_action.dart'; import 'package:tmail_ui_user/features/thread_detail/presentation/extension/get_thread_detail_email_mailbox_contains.dart'; @@ -71,6 +72,8 @@ class ThreadDetailView extends GetWidget { EmailActionType.moveToMailbox, ], emailIsRead: presentationEmail.hasRead, + openBottomSheetContextMenu: controller.mailboxDashBoardController.openBottomSheetContextMenu, + openPopupMenu: controller.mailboxDashBoardController.openPopupMenu, ), optionWidgets: [ if (controller.previousAvailable) diff --git a/lib/main/localizations/app_localizations.dart b/lib/main/localizations/app_localizations.dart index 9f39c96a9..54ee76109 100644 --- a/lib/main/localizations/app_localizations.dart +++ b/lib/main/localizations/app_localizations.dart @@ -4837,4 +4837,13 @@ class AppLocalizations { name: 'view', ); } + + String daysAgo(int days) { + return Intl.plural( + days, + zero: '', + one: ' (1 day ago)', + other: ' ($days days ago)', + ); + } }