diff --git a/core/lib/core.dart b/core/lib/core.dart index 1f4ea1a5e..26e13b64f 100644 --- a/core/lib/core.dart +++ b/core/lib/core.dart @@ -17,6 +17,7 @@ export 'presentation/extensions/tap_down_details_extension.dart'; export 'presentation/extensions/map_extensions.dart'; export 'presentation/extensions/either_view_state_extension.dart'; export 'presentation/extensions/media_type_extension.dart'; +export 'presentation/extensions/scroll_controller_extension.dart'; // Exceptions export 'domain/exceptions/download_file_exception.dart'; diff --git a/core/lib/presentation/extensions/scroll_controller_extension.dart b/core/lib/presentation/extensions/scroll_controller_extension.dart new file mode 100644 index 000000000..a60945729 --- /dev/null +++ b/core/lib/presentation/extensions/scroll_controller_extension.dart @@ -0,0 +1,28 @@ +import 'package:flutter/material.dart'; + +extension ScrollControllerExtension on ScrollController { + void scrollToWidgetTop({ + required GlobalKey key, + double padding = 70, + }) { + final context = key.currentContext; + if (context == null) return; + + final renderBox = context.findRenderObject(); + if (renderBox is! RenderBox) return; + + final offsetY = renderBox.localToGlobal(Offset.zero).dy; + final currentScroll = offset; + + final targetOffset = (currentScroll + offsetY - padding).clamp( + position.minScrollExtent, + position.maxScrollExtent, + ); + + animateTo( + targetOffset, + duration: const Duration(milliseconds: 500), + curve: Curves.easeInOut, + ); + } +} diff --git a/lib/features/email/presentation/action/email_ui_action.dart b/lib/features/email/presentation/action/email_ui_action.dart index b3e9eb924..4aaccda6a 100644 --- a/lib/features/email/presentation/action/email_ui_action.dart +++ b/lib/features/email/presentation/action/email_ui_action.dart @@ -81,6 +81,15 @@ class CollapseEmailInThreadDetailAction extends EmailUIAction { final EmailId emailId; + @override + List get props => [emailId]; +} + +class OpenAttachmentListAction extends EmailUIAction { + OpenAttachmentListAction(this.emailId); + + final EmailId? emailId; + @override List get props => [emailId]; } \ 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 f50b53a2f..a594a6aee 100644 --- a/lib/features/email/presentation/controller/single_email_controller.dart +++ b/lib/features/email/presentation/controller/single_email_controller.dart @@ -189,10 +189,11 @@ class SingleEmailController extends BaseController with AppLoaderMixin { final isEmailContentClipped = RxBool(false); final attendanceStatus = Rxn(); final htmlContentViewKey = GlobalKey(); - final attachmentListKey = GlobalKey(); Identity? _identitySelected; ButtonState? _printEmailButtonState; + GlobalKey? attachmentListKey; + final obxListeners = []; late final EmailActionReactor emailActionReactor; @@ -249,6 +250,9 @@ class SingleEmailController extends BaseController with AppLoaderMixin { @override void onInit() { + if (PlatformInfo.isWeb) { + attachmentListKey = GlobalKey(); + } _threadDetailController = getBinding(); _injectCalendarEventBindings(session, accountId); _registerObxStreamListener(); diff --git a/lib/features/email/presentation/email_view.dart b/lib/features/email/presentation/email_view.dart index f282cc8f3..09b4bfdff 100644 --- a/lib/features/email/presentation/email_view.dart +++ b/lib/features/email/presentation/email_view.dart @@ -450,6 +450,9 @@ class EmailView extends GetWidget { return Obx(() { if (controller.attachments.isNotEmpty) { return EmailAttachmentsWidget( + key: PlatformInfo.isWeb && controller.responsiveUtils.isMobile(context) + ? controller.attachmentListKey + : null, responsiveUtils: controller.responsiveUtils, attachments: controller.attachments, imagePaths: controller.imagePaths, 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 new file mode 100644 index 000000000..de2a4ee92 --- /dev/null +++ b/lib/features/email/presentation/extensions/handle_open_attachment_list_extension.dart @@ -0,0 +1,16 @@ +import 'package:core/presentation/extensions/scroll_controller_extension.dart'; +import 'package:flutter/material.dart'; +import 'package:tmail_ui_user/features/email/presentation/controller/single_email_controller.dart'; + +extension HandleOpenAttachmentListExtension on SingleEmailController { + void jumpToAttachmentList() { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (attachmentListKey == null) return; + + threadDetailController?.scrollController?.scrollToWidgetTop( + key: attachmentListKey!, + padding: 70, + ); + }); + } +} 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 59d23ec40..61f24e264 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,4 +68,8 @@ 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 90181e433..53c9813f5 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 @@ -8,6 +8,7 @@ import 'package:model/email/read_actions.dart'; import 'package:model/mailbox/presentation_mailbox.dart'; import 'package:tmail_ui_user/features/base/widget/popup_menu/popup_menu_item_action_widget.dart'; import 'package:tmail_ui_user/features/destination_picker/presentation/model/destination_picker_arguments.dart'; +import 'package:tmail_ui_user/features/email/presentation/action/email_ui_action.dart'; import 'package:tmail_ui_user/features/email/presentation/model/context_item_email_action.dart'; import 'package:tmail_ui_user/features/email/presentation/model/popup_menu_item_email_action.dart'; import 'package:tmail_ui_user/features/mailbox/presentation/model/mailbox_actions.dart'; @@ -217,4 +218,10 @@ extension OnThreadDetailActionClick on ThreadDetailController { return destinationMailbox.id; } + + void onOpenAttachmentListAction() { + mailboxDashBoardController.dispatchEmailUIAction( + OpenAttachmentListAction(currentExpandedEmailId.value), + ); + } } \ No newline at end of file diff --git a/lib/features/thread_detail/presentation/thread_detail_view.dart b/lib/features/thread_detail/presentation/thread_detail_view.dart index 36da9ee83..96238d0ab 100644 --- a/lib/features/thread_detail/presentation/thread_detail_view.dart +++ b/lib/features/thread_detail/presentation/thread_detail_view.dart @@ -44,6 +44,9 @@ class ThreadDetailView extends GetWidget { threadDetailCanPermanentlyDelete: controller.threadDetailCanPermanentlyDelete, onThreadActionClick: controller.onThreadDetailActionClick, onThreadMoreActionClick: controller.onThreadDetailMoreActionClick, + onOpenAttachmentListAction: controller.isEmailExpandedHasAttachments + ? controller.onOpenAttachmentListAction + : null, optionWidgets: [ if (controller.previousAvailable) TMailButtonWidget.fromIcon( @@ -135,8 +138,11 @@ class ThreadDetailView extends GetWidget { itemCount: manager.isThreadDetailEnabled ? manager.availableThreadIds.length : manager.currentDisplayedEmails.length, - itemBuilder: (context, index) { - return SingleChildScrollView(child: threadBody); + itemBuilder: (_, __) { + return SingleChildScrollView( + controller: controller.scrollController, + child: threadBody, + ); }, onPageChanged: controller.onThreadPageChanged, ), diff --git a/lib/features/thread_detail/presentation/widgets/thread_detail_app_bar.dart b/lib/features/thread_detail/presentation/widgets/thread_detail_app_bar.dart index c8786032b..ef6758f94 100644 --- a/lib/features/thread_detail/presentation/widgets/thread_detail_app_bar.dart +++ b/lib/features/thread_detail/presentation/widgets/thread_detail_app_bar.dart @@ -29,6 +29,7 @@ class ThreadDetailAppBar extends StatelessWidget { this.optionWidgets = const [], this.onThreadActionClick, this.onThreadMoreActionClick, + this.onOpenAttachmentListAction, }); final ResponsiveUtils responsiveUtils; @@ -43,6 +44,7 @@ class ThreadDetailAppBar extends StatelessWidget { final List optionWidgets; final OnThreadActionClick? onThreadActionClick; final OnThreadMoreActionClick? onThreadMoreActionClick; + final VoidCallback? onOpenAttachmentListAction; @override Widget build(BuildContext context) { @@ -114,6 +116,14 @@ class ThreadDetailAppBar extends StatelessWidget { childrenWidgets = [ if (_supportDisplayMailboxNameTitle(context)) backButton, if (isRTL) ...optionWidgets.reversed else ...optionWidgets, + if (onOpenAttachmentListAction != null) + _ThreadDetailAppBarButton( + icon: imagePaths.icAttachment, + tooltipMessage: AppLocalizations.of(context).attachments, + responsiveUtils: responsiveUtils, + iconColor: EmailViewAppBarWidgetStyles.iconColor, + onTapActionCallback: (_) => onOpenAttachmentListAction?.call(), + ), if (isThreadActionAvailable) ...listShortcutActions, ]; } else {