From 7057648b28c1a7a83d7f5e664d54583206d5bf6a Mon Sep 17 00:00:00 2001 From: dab246 Date: Tue, 7 Oct 2025 01:25:19 +0700 Subject: [PATCH] TF-4075 Jump to attachments list position on mobile (cherry picked from commit cd2ca1ec3c5b5160820823905d9f0183536b07e3) --- .../scroll_controller_extension.dart | 14 ++++++ .../presentation/action/email_ui_action.dart | 17 ++++++- .../controller/single_email_controller.dart | 9 +++- ...handle_open_attachment_list_extension.dart | 48 ++++++++++++++++--- .../email/presentation/utils/email_utils.dart | 1 + .../widgets/attachment_item_widget.dart | 2 +- .../widgets/email_attachments_widget.dart | 12 +++-- .../get_thread_detail_action_status.dart | 4 -- .../on_thread_detail_action_click.dart | 10 +++- ...ing_email_opened_properties_extension.dart | 10 ++++ .../thread_detail_controller.dart | 2 + .../presentation/thread_detail_view.dart | 8 ++-- 12 files changed, 112 insertions(+), 25 deletions(-) create mode 100644 lib/features/thread_detail/presentation/extension/parsing_email_opened_properties_extension.dart diff --git a/core/lib/presentation/extensions/scroll_controller_extension.dart b/core/lib/presentation/extensions/scroll_controller_extension.dart index a60945729..5bd2d56c2 100644 --- a/core/lib/presentation/extensions/scroll_controller_extension.dart +++ b/core/lib/presentation/extensions/scroll_controller_extension.dart @@ -25,4 +25,18 @@ extension ScrollControllerExtension on ScrollController { curve: Curves.easeInOut, ); } + + void scrollToBottomWithPadding({required double padding}) { + try { + final maxExtent = position.maxScrollExtent; + + final targetOffset = (maxExtent - padding).clamp(0.0, maxExtent); + + animateTo( + targetOffset, + duration: const Duration(milliseconds: 500), + curve: Curves.easeInOut, + ); + } catch (_) {} + } } diff --git a/lib/features/email/presentation/action/email_ui_action.dart b/lib/features/email/presentation/action/email_ui_action.dart index 4aaccda6a..549ae4d71 100644 --- a/lib/features/email/presentation/action/email_ui_action.dart +++ b/lib/features/email/presentation/action/email_ui_action.dart @@ -86,10 +86,23 @@ class CollapseEmailInThreadDetailAction extends EmailUIAction { } class OpenAttachmentListAction extends EmailUIAction { - OpenAttachmentListAction(this.emailId); + OpenAttachmentListAction({ + required this.emailId, + required this.countAttachments, + required this.screenHeight, + this.isDisplayAllAttachments = false, + }); final EmailId? emailId; + final int countAttachments; + final bool isDisplayAllAttachments; + final double screenHeight; @override - List get props => [emailId]; + List get props => [ + emailId, + countAttachments, + isDisplayAllAttachments, + screenHeight, + ]; } \ No newline at end of file diff --git a/lib/features/email/presentation/controller/single_email_controller.dart b/lib/features/email/presentation/controller/single_email_controller.dart index a594a6aee..f3069f9c3 100644 --- a/lib/features/email/presentation/controller/single_email_controller.dart +++ b/lib/features/email/presentation/controller/single_email_controller.dart @@ -432,7 +432,12 @@ class SingleEmailController extends BaseController with AppLoaderMixin { if (_currentEmailId == null || action.emailId != _currentEmailId) { return; } - jumpToAttachmentList(); + jumpToAttachmentList( + emailId: _currentEmailId!, + countAttachments: action.countAttachments, + screenHeight: action.screenHeight, + isDisplayAllAttachments: action.isDisplayAllAttachments, + ); mailboxDashBoardController.clearEmailUIAction(); } })); @@ -1765,10 +1770,12 @@ class SingleEmailController extends BaseController with AppLoaderMixin { void showAllAttachmentsAction() { isDisplayAllAttachments.value = true; + threadDetailController?.isDisplayAllAttachments = true; } void hideAllAttachmentsAction() { isDisplayAllAttachments.value = false; + threadDetailController?.isDisplayAllAttachments = false; } void _unsubscribeEmail(BuildContext context, PresentationEmail presentationEmail) { diff --git a/lib/features/email/presentation/extensions/handle_open_attachment_list_extension.dart b/lib/features/email/presentation/extensions/handle_open_attachment_list_extension.dart index de2a4ee92..b97e02de9 100644 --- a/lib/features/email/presentation/extensions/handle_open_attachment_list_extension.dart +++ b/lib/features/email/presentation/extensions/handle_open_attachment_list_extension.dart @@ -1,16 +1,50 @@ import 'package:core/presentation/extensions/scroll_controller_extension.dart'; +import 'package:core/utils/app_logger.dart'; +import 'package:core/utils/platform_info.dart'; import 'package:flutter/material.dart'; +import 'package:jmap_dart_client/jmap/mail/email/email.dart'; import 'package:tmail_ui_user/features/email/presentation/controller/single_email_controller.dart'; +import 'package:tmail_ui_user/features/email/presentation/utils/email_utils.dart'; extension HandleOpenAttachmentListExtension on SingleEmailController { - void jumpToAttachmentList() { - WidgetsBinding.instance.addPostFrameCallback((_) { - if (attachmentListKey == null) return; + void jumpToAttachmentList({ + required EmailId emailId, + required int countAttachments, + required double screenHeight, + bool isDisplayAllAttachments = false, + }) { + final scrollController = threadDetailController?.scrollController; + if (scrollController == null || !scrollController.hasClients) { + logError( + '$runtimeType::jumpToAttachmentList(): scrollController is null'); + return; + } - threadDetailController?.scrollController?.scrollToWidgetTop( - key: attachmentListKey!, - padding: 70, - ); + WidgetsBinding.instance.addPostFrameCallback((_) { + if (PlatformInfo.isWeb && attachmentListKey != null) { + scrollController.scrollToWidgetTop( + key: attachmentListKey!, + padding: 70, + ); + } else { + final emailContentHeight = + (screenHeight - 180).clamp(0.0, screenHeight).toDouble(); + + final totalItems = isDisplayAllAttachments + ? countAttachments + 1 + : EmailUtils.maxMobileVisibleAttachments + 1; + + final totalAttachmentsHeight = + totalItems * EmailUtils.attachmentItemHeight + + (totalItems - 1) * EmailUtils.attachmentItemSpacing; + + log('$runtimeType::jumpToAttachmentList(): totalAttachmentsHeight: $totalAttachmentsHeight, emailContentHeight: $emailContentHeight'); + scrollController.scrollToBottomWithPadding( + padding: totalAttachmentsHeight < emailContentHeight + ? 0 + : emailContentHeight, + ); + } }); } } diff --git a/lib/features/email/presentation/utils/email_utils.dart b/lib/features/email/presentation/utils/email_utils.dart index 0e1350954..015cf9a7e 100644 --- a/lib/features/email/presentation/utils/email_utils.dart +++ b/lib/features/email/presentation/utils/email_utils.dart @@ -24,6 +24,7 @@ class EmailUtils { static const double desktopItemMaxWidth = 260; static const double desktopMoreButtonMaxWidth = 70; static const double attachmentItemSpacing = 8; + static const double attachmentItemHeight = 36; static const int maxMobileVisibleAttachments = 3; EmailUtils._(); diff --git a/lib/features/email/presentation/widgets/attachment_item_widget.dart b/lib/features/email/presentation/widgets/attachment_item_widget.dart index 532ce26ae..5f3003ed2 100644 --- a/lib/features/email/presentation/widgets/attachment_item_widget.dart +++ b/lib/features/email/presentation/widgets/attachment_item_widget.dart @@ -109,7 +109,7 @@ class AttachmentItemWidget extends StatelessWidget { ); return TMailContainerWidget( - height: 36, + height: EmailUtils.attachmentItemHeight, borderRadius: 8, border: Border.all(color: AppColor.m3Tertiary70), padding: const EdgeInsets.symmetric(horizontal: 8), diff --git a/lib/features/email/presentation/widgets/email_attachments_widget.dart b/lib/features/email/presentation/widgets/email_attachments_widget.dart index 446939367..0695d3413 100644 --- a/lib/features/email/presentation/widgets/email_attachments_widget.dart +++ b/lib/features/email/presentation/widgets/email_attachments_widget.dart @@ -65,7 +65,7 @@ class EmailAttachmentsWidget extends StatelessWidget { bool isMobile = responsiveUtils.isMobile(context); final hideButton = SizedBox( - height: 36, + height: EmailUtils.attachmentItemHeight, width: isMobile ? double.infinity : null, child: ConfirmDialogButton( label: AppLocalizations.of(context).hideAll, @@ -105,7 +105,9 @@ class EmailAttachmentsWidget extends StatelessWidget { return AttachmentItemWidget( attachment: attachment, imagePaths: imagePaths, - margin: const EdgeInsets.only(top: 8), + margin: const EdgeInsets.only( + top: EmailUtils.attachmentItemSpacing, + ), downloadAttachmentAction: downloadAttachmentAction, viewAttachmentAction: viewAttachmentAction, singleEmailControllerTag: singleEmailControllerTag, @@ -113,10 +115,10 @@ class EmailAttachmentsWidget extends StatelessWidget { }).toList(), ), ), - const SizedBox(height: 8), + const SizedBox(height: EmailUtils.attachmentItemSpacing), if (hiddenItemsCount > 0) SizedBox( - height: 36, + height: EmailUtils.attachmentItemHeight, width: double.infinity, child: ConfirmDialogButton( label: AppLocalizations.of(context).moreAttachments( @@ -200,7 +202,7 @@ class EmailAttachmentsWidget extends StatelessWidget { }).toList(), if (hiddenItemsCount > 0) SizedBox( - height: 36, + height: EmailUtils.attachmentItemHeight, child: ConfirmDialogButton( label: '+$hiddenItemsCount', backgroundColor: Theme.of(context).colorScheme.outline.withValues( diff --git a/lib/features/thread_detail/presentation/extension/get_thread_detail_action_status.dart b/lib/features/thread_detail/presentation/extension/get_thread_detail_action_status.dart index 61f24e264..59d23ec40 100644 --- a/lib/features/thread_detail/presentation/extension/get_thread_detail_action_status.dart +++ b/lib/features/thread_detail/presentation/extension/get_thread_detail_action_status.dart @@ -68,8 +68,4 @@ extension GetThreadDetailActionStatus on ThreadDetailController { bool get threadDetailCanPermanentlyDelete { return threadDetailIsTrashed || threadDetailIsSpam || threadDetailIsDraft; } - - bool get isEmailExpandedHasAttachments { - return currentEmailLoaded.value?.attachments.isNotEmpty == true; - } } diff --git a/lib/features/thread_detail/presentation/extension/on_thread_detail_action_click.dart b/lib/features/thread_detail/presentation/extension/on_thread_detail_action_click.dart index 53c9813f5..96fa5ed2d 100644 --- a/lib/features/thread_detail/presentation/extension/on_thread_detail_action_click.dart +++ b/lib/features/thread_detail/presentation/extension/on_thread_detail_action_click.dart @@ -18,6 +18,7 @@ import 'package:tmail_ui_user/features/thread/domain/state/mark_as_multiple_emai import 'package:tmail_ui_user/features/thread/domain/state/mark_as_star_multiple_email_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_action_status.dart'; +import 'package:tmail_ui_user/features/thread_detail/presentation/extension/parsing_email_opened_properties_extension.dart'; import 'package:tmail_ui_user/features/thread_detail/presentation/thread_detail_controller.dart'; import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; import 'package:tmail_ui_user/main/routes/app_routes.dart'; @@ -219,9 +220,14 @@ extension OnThreadDetailActionClick on ThreadDetailController { return destinationMailbox.id; } - void onOpenAttachmentListAction() { + void onOpenAttachmentListAction(double screenHeight) { mailboxDashBoardController.dispatchEmailUIAction( - OpenAttachmentListAction(currentExpandedEmailId.value), + OpenAttachmentListAction( + emailId: currentExpandedEmailId.value, + countAttachments: currentAttachmentsList.length, + screenHeight: screenHeight, + isDisplayAllAttachments: isDisplayAllAttachments, + ), ); } } \ No newline at end of file diff --git a/lib/features/thread_detail/presentation/extension/parsing_email_opened_properties_extension.dart b/lib/features/thread_detail/presentation/extension/parsing_email_opened_properties_extension.dart new file mode 100644 index 000000000..96b807ffc --- /dev/null +++ b/lib/features/thread_detail/presentation/extension/parsing_email_opened_properties_extension.dart @@ -0,0 +1,10 @@ +import 'package:model/email/attachment.dart'; +import 'package:tmail_ui_user/features/thread_detail/presentation/thread_detail_controller.dart'; + +extension ParsingEmailOpenedPropertiesExtension on ThreadDetailController { + List get currentAttachmentsList => + currentEmailLoaded.value?.attachments ?? []; + + bool get isEmailExpandedHasAttachments => + currentAttachmentsList.isNotEmpty == true; +} diff --git a/lib/features/thread_detail/presentation/thread_detail_controller.dart b/lib/features/thread_detail/presentation/thread_detail_controller.dart index a4316abd1..f55a507cd 100644 --- a/lib/features/thread_detail/presentation/thread_detail_controller.dart +++ b/lib/features/thread_detail/presentation/thread_detail_controller.dart @@ -123,6 +123,7 @@ class ThreadDetailController extends BaseController { ScrollController? scrollController; CreateNewEmailRuleFilterInteractor? _createNewEmailRuleFilterInteractor; bool loadThreadOnThreadChanged = false; + bool isDisplayAllAttachments = false; AccountId? get accountId => mailboxDashBoardController.accountId.value; Session? get session => mailboxDashBoardController.sessionCurrent; @@ -232,6 +233,7 @@ class ThreadDetailController extends BaseController { currentEmailLoaded.value = null; cachedEmailLoaded.clear(); _threadGetDebouncer.value = null; + isDisplayAllAttachments = false; } @override diff --git a/lib/features/thread_detail/presentation/thread_detail_view.dart b/lib/features/thread_detail/presentation/thread_detail_view.dart index 96238d0ab..0b5e0479e 100644 --- a/lib/features/thread_detail/presentation/thread_detail_view.dart +++ b/lib/features/thread_detail/presentation/thread_detail_view.dart @@ -11,19 +11,19 @@ import 'package:tmail_ui_user/features/email/presentation/action/email_ui_action 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/manage_account/presentation/extensions/vacation_response_extension.dart'; +import 'package:tmail_ui_user/features/manage_account/presentation/vacation/widgets/vacation_notification_message_widget.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_action_status.dart'; import 'package:tmail_ui_user/features/thread_detail/presentation/extension/get_thread_details_email_views.dart'; import 'package:tmail_ui_user/features/thread_detail/presentation/extension/on_thread_detail_action_click.dart'; import 'package:tmail_ui_user/features/thread_detail/presentation/extension/on_thread_page_changed.dart'; +import 'package:tmail_ui_user/features/thread_detail/presentation/extension/parsing_email_opened_properties_extension.dart'; import 'package:tmail_ui_user/features/thread_detail/presentation/thread_detail_controller.dart'; import 'package:tmail_ui_user/features/thread_detail/presentation/widgets/thread_detail_app_bar.dart'; import 'package:tmail_ui_user/features/thread_detail/presentation/widgets/thread_detail_cupertino_loading_widget.dart'; import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; -import '../../manage_account/presentation/vacation/widgets/vacation_notification_message_widget.dart'; - class ThreadDetailView extends GetWidget { const ThreadDetailView({super.key}); @@ -45,7 +45,9 @@ class ThreadDetailView extends GetWidget { onThreadActionClick: controller.onThreadDetailActionClick, onThreadMoreActionClick: controller.onThreadDetailMoreActionClick, onOpenAttachmentListAction: controller.isEmailExpandedHasAttachments - ? controller.onOpenAttachmentListAction + ? () => controller.onOpenAttachmentListAction( + controller.responsiveUtils.getSizeScreenHeight(context), + ) : null, optionWidgets: [ if (controller.previousAvailable)