TF-4075 Jump to attachments list position on web responsive mobile

This commit is contained in:
dab246
2025-10-06 19:43:32 +07:00
committed by Dat H. Pham
parent 5f3098484b
commit 3478e2aabc
10 changed files with 91 additions and 3 deletions
+1
View File
@@ -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';
@@ -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,
);
}
}
@@ -81,6 +81,15 @@ class CollapseEmailInThreadDetailAction extends EmailUIAction {
final EmailId emailId;
@override
List<Object?> get props => [emailId];
}
class OpenAttachmentListAction extends EmailUIAction {
OpenAttachmentListAction(this.emailId);
final EmailId? emailId;
@override
List<Object?> get props => [emailId];
}
@@ -189,10 +189,11 @@ class SingleEmailController extends BaseController with AppLoaderMixin {
final isEmailContentClipped = RxBool(false);
final attendanceStatus = Rxn<AttendanceStatus>();
final htmlContentViewKey = GlobalKey<HtmlContentViewState>();
final attachmentListKey = GlobalKey();
Identity? _identitySelected;
ButtonState? _printEmailButtonState;
GlobalKey? attachmentListKey;
final obxListeners = <Worker>[];
late final EmailActionReactor emailActionReactor;
@@ -249,6 +250,9 @@ class SingleEmailController extends BaseController with AppLoaderMixin {
@override
void onInit() {
if (PlatformInfo.isWeb) {
attachmentListKey = GlobalKey();
}
_threadDetailController = getBinding<ThreadDetailController>();
_injectCalendarEventBindings(session, accountId);
_registerObxStreamListener();
@@ -450,6 +450,9 @@ class EmailView extends GetWidget<SingleEmailController> {
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,
@@ -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,
);
});
}
}
@@ -68,4 +68,8 @@ extension GetThreadDetailActionStatus on ThreadDetailController {
bool get threadDetailCanPermanentlyDelete {
return threadDetailIsTrashed || threadDetailIsSpam || threadDetailIsDraft;
}
bool get isEmailExpandedHasAttachments {
return currentEmailLoaded.value?.attachments.isNotEmpty == true;
}
}
@@ -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),
);
}
}
@@ -44,6 +44,9 @@ class ThreadDetailView extends GetWidget<ThreadDetailController> {
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<ThreadDetailController> {
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,
),
@@ -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<Widget> 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 {