diff --git a/core/lib/presentation/views/html_viewer/html_content_viewer_widget.dart b/core/lib/presentation/views/html_viewer/html_content_viewer_widget.dart index 5ccac6cac..1ada91dec 100644 --- a/core/lib/presentation/views/html_viewer/html_content_viewer_widget.dart +++ b/core/lib/presentation/views/html_viewer/html_content_viewer_widget.dart @@ -360,6 +360,7 @@ class HtmlContentViewState extends State with AutomaticKeepAl void dispose() { _loadingBarNotifier.dispose(); _htmlData = null; + _webViewController.dispose(); super.dispose(); } diff --git a/lib/features/email/presentation/controller/single_email_controller.dart b/lib/features/email/presentation/controller/single_email_controller.dart index 991767bd6..8f388b9ca 100644 --- a/lib/features/email/presentation/controller/single_email_controller.dart +++ b/lib/features/email/presentation/controller/single_email_controller.dart @@ -127,6 +127,7 @@ import 'package:tmail_ui_user/features/thread_detail/presentation/action/thread_ 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/focus_thread_detail_expanded_email.dart'; import 'package:tmail_ui_user/features/thread_detail/presentation/extension/mark_collapsed_email_unread_success.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/update_cached_list_email_loaded.dart'; import 'package:tmail_ui_user/features/thread_detail/presentation/thread_detail_controller.dart'; import 'package:tmail_ui_user/main/error/capability_validator.dart'; @@ -204,6 +205,9 @@ class SingleEmailController extends BaseController with AppLoaderMixin { : _threadDetailController?.emailIdsPresentation[_currentEmailId]; } + bool get isOnlyEmailInThread => + _threadDetailController?.emailIdsPresentation.length == 1; + bool get calendarEventProcessing => viewState.value.fold( (failure) => false, (success) => success is CalendarEventReplying); @@ -680,14 +684,7 @@ class SingleEmailController extends BaseController with AppLoaderMixin { emailUnsubscribe.value = null; } } - if (currentEmail?.threadId != null && - currentEmail?.id == mailboxDashBoardController.selectedEmail.value?.id) { - mailboxDashBoardController.dispatchThreadDetailUIAction( - LoadThreadDetailAfterSelectedEmailAction( - currentEmail!.threadId!, - ) - ); - } + _loadThreadOnGetEmailContentSuccess(); } void _getEmailContentSuccess(GetEmailContentSuccess success) { @@ -758,9 +755,14 @@ class SingleEmailController extends BaseController with AppLoaderMixin { if ((_threadDetailController?.emailIdsPresentation.keys.length ?? 0) > 1 == true) { _jumpScrollViewToTopOfEmail(); } + _loadThreadOnGetEmailContentSuccess(); + } + + void _loadThreadOnGetEmailContentSuccess() { if (currentEmail?.threadId != null && currentEmail?.id == mailboxDashBoardController.selectedEmail.value?.id && - success is! GetEmailContentFromThreadCacheSuccess) { + _threadDetailController?.loadThreadOnThreadChanged == true) { + _threadDetailController?.loadThreadOnThreadChanged = false; mailboxDashBoardController.dispatchThreadDetailUIAction( LoadThreadDetailAfterSelectedEmailAction( currentEmail!.threadId!, @@ -2391,4 +2393,12 @@ class SingleEmailController extends BaseController with AppLoaderMixin { } ); } + + void onScrollHorizontalEnd(bool isLeftDirection) { + if (isLeftDirection) { + _threadDetailController?.onPreviousMobile(); + } else { + _threadDetailController?.onNextMobile(); + } + } } \ No newline at end of file diff --git a/lib/features/email/presentation/email_view.dart b/lib/features/email/presentation/email_view.dart index e48cc48bf..4a775c379 100644 --- a/lib/features/email/presentation/email_view.dart +++ b/lib/features/email/presentation/email_view.dart @@ -214,7 +214,7 @@ class EmailView extends GetWidget { ); } - if (controller.currentEmail == null || !isInsideThreadDetailView) { + if (controller.currentEmail == null || isFirstEmailInThreadDetail) { return const BoxDecoration(color: Colors.white); } @@ -278,35 +278,38 @@ class EmailView extends GetWidget { onEmailActionClick: (presentationEmail, actionType) => controller.handleEmailAction(context, presentationEmail, actionType), isInsideThreadDetailView: isInsideThreadDetailView, emailLoaded: controller.currentEmailLoaded.value, - onMoreActionClick: (presentationEmail, position) => controller.emailActionReactor.handleMoreEmailAction( - mailboxContain: controller.getMailboxContain(presentationEmail), - presentationEmail: presentationEmail, - position: position, - responsiveUtils: controller.responsiveUtils, - imagePaths: controller.imagePaths, - username: controller.session?.username, - handleEmailAction: (email, action) => controller.handleEmailAction(context, email, action), - additionalActions: [ - EmailActionType.forward, - EmailActionType.replyAll, - EmailActionType.replyToList, - EmailActionType.printAll, - if (controller.responsiveUtils.isMobile(context)) - EmailActionType.moveToMailbox, - if (!controller.responsiveUtils.isDesktop(context)) ...[ - EmailActionType.markAsStarred, - EmailActionType.unMarkAsStarred, - EmailActionType.moveToTrash, - EmailActionType.deletePermanently, - ], - ], - emailIsRead: presentationEmail.hasRead, - ), + onMoreActionClick: controller.isOnlyEmailInThread + ? null + : (presentationEmail, position) => controller.emailActionReactor.handleMoreEmailAction( + mailboxContain: controller.getMailboxContain(presentationEmail), + presentationEmail: presentationEmail, + position: position, + responsiveUtils: controller.responsiveUtils, + imagePaths: controller.imagePaths, + username: controller.session?.username, + handleEmailAction: (email, action) => controller.handleEmailAction(context, email, action), + additionalActions: [ + EmailActionType.forward, + EmailActionType.replyAll, + EmailActionType.replyToList, + EmailActionType.printAll, + if (controller.responsiveUtils.isMobile(context)) + EmailActionType.moveToMailbox, + if (!controller.responsiveUtils.isDesktop(context)) ...[ + EmailActionType.markAsStarred, + EmailActionType.unMarkAsStarred, + EmailActionType.moveToTrash, + EmailActionType.deletePermanently, + ], + ], + emailIsRead: presentationEmail.hasRead, + ), onToggleThreadDetailCollapseExpand: onToggleThreadDetailCollapseExpand, onTapAvatarActionClick: onToggleThreadDetailCollapseExpand, mailboxContain: presentationEmail.findMailboxContain( controller.mailboxDashBoardController.mapMailboxById, ), + isOnlyEmailInThread: controller.isOnlyEmailInThread, )), Obx(() => MailUnsubscribedBanner( presentationEmail: controller.currentEmail, @@ -449,6 +452,7 @@ class EmailView extends GetWidget { maxHtmlContentHeight: ConstantsUI.htmlContentMaxHeight, onMailtoDelegateAction: controller.openMailToLink, onHtmlContentClippedAction: controller.onHtmlContentClippedAction, + onScrollHorizontalEnd: controller.onScrollHorizontalEnd, keepAlive: isInsideThreadDetailView, enableQuoteToggle: isInsideThreadDetailView, ); @@ -489,6 +493,7 @@ class EmailView extends GetWidget { onMailtoDelegateAction: controller.openMailToLink, keepAlive: isInsideThreadDetailView, enableQuoteToggle: isInsideThreadDetailView, + onScrollHorizontalEnd: controller.onScrollHorizontalEnd, ); }) ); 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 182cb9a42..db1e47456 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 @@ -548,7 +548,7 @@ class EmailActionReactor with MessageDialogActionMixin, PopupContextMenuActionMi : EmailActionType.markAsStarred, if (additionalActions.contains(EmailActionType.moveToTrash) && additionalActions.contains(EmailActionType.deletePermanently)) - _canDeletePermanently(presentationEmail) + _canDeletePermanently(presentationEmail, mailboxContain) ? EmailActionType.deletePermanently : EmailActionType.moveToTrash, if (emailIsRead) @@ -598,8 +598,13 @@ class EmailActionReactor with MessageDialogActionMixin, PopupContextMenuActionMi } } - bool _canDeletePermanently(PresentationEmail email) { - return email.mailboxContain?.isTrash + bool _canDeletePermanently( + PresentationEmail email, + PresentationMailbox? mailboxContain, + ) { + return mailboxContain?.isTrash + ?? mailboxContain?.isSpam + ?? email.mailboxContain?.isTrash ?? email.mailboxContain?.isSpam ?? false; } 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 34b6ab842..cf1b5eac3 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 @@ -34,6 +34,7 @@ class EmailViewAppBarWidget extends StatelessWidget { final EdgeInsetsGeometry? iconPadding; final EdgeInsetsGeometry? iconMargin; final EdgeInsetsGeometry? padding; + final bool isOnlyEmailInThread; EmailViewAppBarWidget({ Key? key, @@ -51,6 +52,7 @@ class EmailViewAppBarWidget extends StatelessWidget { this.iconPadding, this.iconMargin, this.padding, + this.isOnlyEmailInThread = false, }) : super(key: key); @override @@ -251,19 +253,22 @@ class EmailViewAppBarWidget extends StatelessWidget { _getMoveEmailButton(appLocalizations), _getMarkStarButton(appLocalizations), _getDeleteButton(appLocalizations), - _getMoreButton(appLocalizations, isScreenWithShortestSide), + if (onMoreActionClick != null) + _getMoreButton(appLocalizations, isScreenWithShortestSide), ]; } return [ - _getReplyButton(appLocalizations), + if (!isOnlyEmailInThread) + _getReplyButton(appLocalizations), if (!isResponsiveMobile) _getMoveEmailButton(appLocalizations), if (isResponsiveDesktop) ...[ _getMarkStarButton(appLocalizations), _getDeleteButton(appLocalizations), ], - _getMoreButton(appLocalizations, isScreenWithShortestSide), + if (onMoreActionClick != null) + _getMoreButton(appLocalizations, isScreenWithShortestSide), ]; } } \ No newline at end of file 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 a03aa2ec5..0710c3bbd 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 @@ -5,7 +5,6 @@ import 'package:core/presentation/utils/responsive_utils.dart'; import 'package:core/presentation/utils/theme_utils.dart'; import 'package:core/presentation/views/button/tmail_button_widget.dart'; import 'package:core/presentation/views/image/avatar_builder.dart'; -import 'package:core/utils/platform_info.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter_svg/flutter_svg.dart'; @@ -42,6 +41,7 @@ class InformationSenderAndReceiverBuilder extends StatelessWidget { final OnTapAvatarActionClick? onTapAvatarActionClick; final PresentationMailbox? mailboxContain; final bool showUnreadVisualization; + final bool isOnlyEmailInThread; const InformationSenderAndReceiverBuilder({ Key? key, @@ -61,6 +61,7 @@ class InformationSenderAndReceiverBuilder extends StatelessWidget { this.onTapAvatarActionClick, this.mailboxContain, this.showUnreadVisualization = false, + this.isOnlyEmailInThread = false, }) : super(key: key); @override @@ -164,9 +165,7 @@ class InformationSenderAndReceiverBuilder extends StatelessWidget { SizedBox( height: IconUtils.defaultIconSize, child: OverflowBox( - maxHeight: PlatformInfo.isIOS - ? EmailViewAppBarWidgetStyles.heightIOS(context, responsiveUtils) - : EmailViewAppBarWidgetStyles.height, + maxHeight: EmailViewAppBarWidgetStyles.height, fit: OverflowBoxFit.deferToChild, child: EmailViewAppBarWidget( key: const Key('email_view_app_bar_widget'), @@ -183,6 +182,7 @@ class InformationSenderAndReceiverBuilder extends StatelessWidget { iconPadding: const EdgeInsets.all(8), iconMargin: EdgeInsets.zero, padding: EdgeInsets.zero, + isOnlyEmailInThread: isOnlyEmailInThread, ), ), ), diff --git a/lib/features/thread_detail/domain/state/get_thread_by_id_state.dart b/lib/features/thread_detail/domain/state/get_thread_by_id_state.dart index 6903c0475..553999919 100644 --- a/lib/features/thread_detail/domain/state/get_thread_by_id_state.dart +++ b/lib/features/thread_detail/domain/state/get_thread_by_id_state.dart @@ -13,16 +13,28 @@ class GettingThreadById extends LoadingState { class GetThreadByIdSuccess extends UIState { final List emailIds; + final ThreadId? threadId; final bool updateCurrentThreadDetail; + final bool skipLoadingMetadata; - GetThreadByIdSuccess(this.emailIds, {this.updateCurrentThreadDetail = false}); + GetThreadByIdSuccess( + this.emailIds, { + required this.threadId, + this.updateCurrentThreadDetail = false, + this.skipLoadingMetadata = false, + }); @override - List get props => [emailIds, updateCurrentThreadDetail]; + List get props => [ + emailIds, + threadId, + updateCurrentThreadDetail, + skipLoadingMetadata, + ]; } class PreloadEmailIdsInThreadSuccess extends GetThreadByIdSuccess { - PreloadEmailIdsInThreadSuccess(super.emailIds); + PreloadEmailIdsInThreadSuccess(super.emailIds, {required super.threadId}); } class GetThreadByIdFailure extends FeatureFailure { diff --git a/lib/features/thread_detail/domain/usecases/get_thread_by_id_interactor.dart b/lib/features/thread_detail/domain/usecases/get_thread_by_id_interactor.dart index 61e69080a..57ed3a78c 100644 --- a/lib/features/thread_detail/domain/usecases/get_thread_by_id_interactor.dart +++ b/lib/features/thread_detail/domain/usecases/get_thread_by_id_interactor.dart @@ -38,6 +38,7 @@ class GetThreadByIdInteractor { yield Right(GetThreadByIdSuccess( result, + threadId: threadId, updateCurrentThreadDetail: updateCurrentThreadDetail, )); } catch (e) { diff --git a/lib/features/thread_detail/presentation/extension/get_thread_detail_loading_view.dart b/lib/features/thread_detail/presentation/extension/get_thread_detail_loading_view.dart deleted file mode 100644 index 9157ac036..000000000 --- a/lib/features/thread_detail/presentation/extension/get_thread_detail_loading_view.dart +++ /dev/null @@ -1,36 +0,0 @@ -import 'package:core/presentation/views/loading/cupertino_loading_widget.dart'; -import 'package:flutter/material.dart'; -import 'package:tmail_ui_user/features/thread_detail/presentation/thread_detail_controller.dart'; - -extension GetThreadDetailLoadingView on ThreadDetailController { - Widget getThreadDetailLoadingView({ - required bool isResponsiveDesktop, - required bool isLoading, - bool isExpanded = true, - }) { - if (!isLoading) return const SizedBox.shrink(); - - final child = Container( - alignment: Alignment.center, - decoration: const BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.all(Radius.circular(20)), - ), - margin: isResponsiveDesktop - ? const EdgeInsetsDirectional.only(end: 16) - : null, - padding: const EdgeInsets.only(top: 16), - child: const SizedBox( - width: 24, - height: 24, - child: CupertinoLoadingWidget(), - ), - ); - - if (isExpanded) { - return Expanded(child: child); - } else { - return child; - } - } -} \ 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 dce0e5be6..94f8369db 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 @@ -98,16 +98,29 @@ extension GetThreadDetailEmailViews on ThreadDetailController { ); } + if (isFirstEmailInThreadDetail) { + return Padding( + padding: const EdgeInsetsDirectional.only(bottom: 16), + child: EmailView( + key: GlobalObjectKey('${presentationEmail.id?.id.value ?? ''}firstInThread'), + isInsideThreadDetailView: true, + emailId: presentationEmail.id, + isFirstEmailInThreadDetail: true, + threadSubject: emailIdsPresentation.values.last?.subject, + onToggleThreadDetailCollapseExpand: () { + toggleThreadDetailCollapeExpand(presentationEmail); + }, + scrollController: scrollController, + ), + ); + } + return Padding( padding: const EdgeInsetsDirectional.only(bottom: 16), child: EmailView( key: GlobalObjectKey(presentationEmail.id?.id.value ?? ''), - isInsideThreadDetailView: emailIdsPresentation.length > 1, + isInsideThreadDetailView: true, emailId: presentationEmail.id, - isFirstEmailInThreadDetail: isFirstEmailInThreadDetail, - threadSubject: isFirstEmailInThreadDetail - ? emailIdsPresentation.values.last?.subject - : null, onToggleThreadDetailCollapseExpand: () { toggleThreadDetailCollapeExpand(presentationEmail); }, diff --git a/lib/features/thread_detail/presentation/extension/handle_get_email_ids_by_thread_id_success.dart b/lib/features/thread_detail/presentation/extension/handle_get_email_ids_by_thread_id_success.dart index 640553726..a59651caf 100644 --- a/lib/features/thread_detail/presentation/extension/handle_get_email_ids_by_thread_id_success.dart +++ b/lib/features/thread_detail/presentation/extension/handle_get_email_ids_by_thread_id_success.dart @@ -5,7 +5,8 @@ extension HandleGetEmailIdsByThreadIdSuccess on ThreadDetailController { void handleGetEmailIdsByThreadIdSuccess( GetThreadByIdSuccess success, ) { - if (success.emailIds.isEmpty) { + final currentThreadId = mailboxDashBoardController.selectedEmail.value?.threadId; + if (success.emailIds.isEmpty || success.threadId != currentThreadId) { return; } diff --git a/lib/features/thread_detail/presentation/extension/handle_get_emails_by_ids_success.dart b/lib/features/thread_detail/presentation/extension/handle_get_emails_by_ids_success.dart index 318a0b2b6..ba12b247b 100644 --- a/lib/features/thread_detail/presentation/extension/handle_get_emails_by_ids_success.dart +++ b/lib/features/thread_detail/presentation/extension/handle_get_emails_by_ids_success.dart @@ -37,6 +37,7 @@ extension HandleGetEmailsByIdsSuccess on ThreadDetailController { : EmailInThreadStatus.collapsed, ); } + threadDetailManager.currentMobilePageViewIndex.refresh(); if (_skipScrollJump(isLoadMore)) return; diff --git a/lib/features/thread_detail/presentation/extension/handle_refresh_thread_detail_action.dart b/lib/features/thread_detail/presentation/extension/handle_refresh_thread_detail_action.dart index e619c8af3..a652068ec 100644 --- a/lib/features/thread_detail/presentation/extension/handle_refresh_thread_detail_action.dart +++ b/lib/features/thread_detail/presentation/extension/handle_refresh_thread_detail_action.dart @@ -32,6 +32,7 @@ extension HandleRefreshThreadDetailAction on ThreadDetailController { consumeState(Stream.value(Right(GetThreadByIdSuccess( [currentEmailId], updateCurrentThreadDetail: true, + threadId: mailboxDashBoardController.selectedEmail.value?.threadId, )))); } @@ -74,6 +75,7 @@ extension HandleRefreshThreadDetailAction on ThreadDetailController { consumeState(Stream.value(Right(GetThreadByIdSuccess( afterRefreshedEmailIds, + threadId: currentThreadId, updateCurrentThreadDetail: true, )))); consumeState(Stream.value(Right(GetEmailsByIdsSuccess( diff --git a/lib/features/thread_detail/presentation/extension/initialize_thread_detail_emails.dart b/lib/features/thread_detail/presentation/extension/initialize_thread_detail_emails.dart index 30b06699b..a22b88093 100644 --- a/lib/features/thread_detail/presentation/extension/initialize_thread_detail_emails.dart +++ b/lib/features/thread_detail/presentation/extension/initialize_thread_detail_emails.dart @@ -9,6 +9,9 @@ import 'package:tmail_ui_user/features/thread_detail/presentation/utils/thread_d extension InitializeThreadDetailEmails on ThreadDetailController { void initializeThreadDetailEmails(GetThreadByIdSuccess success) { + final currentThreadId = mailboxDashBoardController.selectedEmail.value?.threadId; + if (success.skipLoadingMetadata || currentThreadId != success.threadId) return; + final selectedEmailId = mailboxDashBoardController.selectedEmail.value?.id; if (skipLoadThreadMetaData( selectedEmailId: selectedEmailId, diff --git a/lib/features/thread_detail/presentation/extension/initialize_thread_detail_manager.dart b/lib/features/thread_detail/presentation/extension/initialize_thread_detail_manager.dart new file mode 100644 index 000000000..2eb9e8d05 --- /dev/null +++ b/lib/features/thread_detail/presentation/extension/initialize_thread_detail_manager.dart @@ -0,0 +1,34 @@ +import 'package:collection/collection.dart'; +import 'package:flutter/material.dart'; +import 'package:model/email/presentation_email.dart'; +import 'package:tmail_ui_user/features/thread_detail/presentation/thread_detail_manager.dart'; + +extension InitializeThreadDetailManager on ThreadDetailManager { + void initializeThreadDetailManager( + List currentDisplayedEmails, + ) { + availableThreadIds.value = currentDisplayedEmails + .map((presentationEmail) => presentationEmail.threadId) + .toSet() + .whereNotNull() + .toList(); + if (availableThreadIds.isEmpty) return; + + final selectedEmail = mailboxDashBoardController.selectedEmail.value; + if (isThreadDetailEnabled && selectedEmail?.threadId != null) { + final currentThreadIndex = availableThreadIds.indexOf( + selectedEmail!.threadId!, + ); + currentMobilePageViewIndex.value = currentThreadIndex; + } else if (selectedEmail?.id != null) { + final currentEmailIndex = currentDisplayedEmails.indexOf( + selectedEmail!, + ); + currentMobilePageViewIndex.value = currentEmailIndex; + } + + pageController ??= PageController( + initialPage: currentMobilePageViewIndex.value, + ); + } +} \ No newline at end of file diff --git a/lib/features/thread_detail/presentation/extension/on_thread_page_changed.dart b/lib/features/thread_detail/presentation/extension/on_thread_page_changed.dart new file mode 100644 index 000000000..4679af366 --- /dev/null +++ b/lib/features/thread_detail/presentation/extension/on_thread_page_changed.dart @@ -0,0 +1,66 @@ +import 'package:core/utils/platform_info.dart'; +import 'package:flutter/animation.dart'; +import 'package:tmail_ui_user/features/email/presentation/action/email_ui_action.dart'; +import 'package:tmail_ui_user/features/thread_detail/presentation/extension/thread_detail_next_previous_actions.dart'; +import 'package:tmail_ui_user/features/thread_detail/presentation/thread_detail_controller.dart'; + +extension OnThreadPageChanged on ThreadDetailController { + void onThreadPageChanged(int index) { + if (index > threadDetailManager.currentMobilePageViewIndex.value) { + onNext(); + } else if (index < threadDetailManager.currentMobilePageViewIndex.value) { + onPrevious(); + } + } + + bool get nextAvailable => + PlatformInfo.isWeb && threadDetailManager.nextAvailable; + bool get previousAvailable => + PlatformInfo.isWeb && threadDetailManager.previousAvailable; + + void onNext() { + if (currentExpandedEmailId.value != null) { + mailboxDashBoardController.dispatchEmailUIAction( + CollapseEmailInThreadDetailAction(currentExpandedEmailId.value!), + ); + } + loadThreadOnThreadChanged = isThreadDetailEnabled; + threadDetailManager.onNext(); + } + + void onNextMobile() { + if (currentExpandedEmailId.value != null) { + mailboxDashBoardController.dispatchEmailUIAction( + CollapseEmailInThreadDetailAction(currentExpandedEmailId.value!), + ); + } + loadThreadOnThreadChanged = isThreadDetailEnabled; + threadDetailManager.pageController?.nextPage( + duration: const Duration(milliseconds: 500), + curve: Curves.linear, + ); + } + + void onPrevious() { + if (currentExpandedEmailId.value != null) { + mailboxDashBoardController.dispatchEmailUIAction( + CollapseEmailInThreadDetailAction(currentExpandedEmailId.value!), + ); + } + loadThreadOnThreadChanged = isThreadDetailEnabled; + threadDetailManager.onPrevious(); + } + + void onPreviousMobile() { + if (currentExpandedEmailId.value != null) { + mailboxDashBoardController.dispatchEmailUIAction( + CollapseEmailInThreadDetailAction(currentExpandedEmailId.value!), + ); + } + loadThreadOnThreadChanged = isThreadDetailEnabled; + threadDetailManager.pageController?.previousPage( + duration: const Duration(milliseconds: 500), + curve: Curves.linear, + ); + } +} diff --git a/lib/features/thread_detail/presentation/extension/refresh_thread_detail_on_setting_changed.dart b/lib/features/thread_detail/presentation/extension/refresh_thread_detail_on_setting_changed.dart index 3aa53901f..5fa7fd56d 100644 --- a/lib/features/thread_detail/presentation/extension/refresh_thread_detail_on_setting_changed.dart +++ b/lib/features/thread_detail/presentation/extension/refresh_thread_detail_on_setting_changed.dart @@ -1,6 +1,6 @@ -import 'package:tmail_ui_user/features/thread_detail/presentation/thread_detail_controller.dart'; +import 'package:tmail_ui_user/features/thread_detail/presentation/thread_detail_manager.dart'; -extension RefreshThreadDetailOnSettingChanged on ThreadDetailController { +extension RefreshThreadDetailOnSettingChanged on ThreadDetailManager { void refreshThreadDetailOnSettingChanged() { if (threadDetailWasEnabled != isThreadDetailEnabled) { threadDetailWasEnabled = isThreadDetailEnabled; diff --git a/lib/features/thread_detail/presentation/extension/thread_detail_next_previous_actions.dart b/lib/features/thread_detail/presentation/extension/thread_detail_next_previous_actions.dart new file mode 100644 index 000000000..d6558cdb7 --- /dev/null +++ b/lib/features/thread_detail/presentation/extension/thread_detail_next_previous_actions.dart @@ -0,0 +1,70 @@ +import 'package:collection/collection.dart'; +import 'package:core/utils/platform_info.dart'; +import 'package:flutter/widgets.dart'; +import 'package:model/email/presentation_email.dart'; +import 'package:tmail_ui_user/features/thread_detail/presentation/thread_detail_manager.dart'; +import 'package:tmail_ui_user/main/routes/route_utils.dart'; + +extension ThreadDetailNextPreviousActions on ThreadDetailManager { + bool get nextAvailable => isThreadDetailEnabled + ? currentThreadId != availableThreadIds.lastOrNull + : currentEmailId != currentDisplayedEmails.lastOrNull?.id; + + void onNext() { + if (!nextAvailable) return; + + final currentIndex = isThreadDetailEnabled + ? availableThreadIds.indexOf(currentThreadId!) + : currentDisplayedEmails + .indexOf(mailboxDashBoardController.selectedEmail.value!); + _navigate(currentIndex + 1, isThreadDetailEnabled); + } + + bool get previousAvailable => isThreadDetailEnabled + ? currentThreadId != availableThreadIds.firstOrNull + : currentEmailId != currentDisplayedEmails.firstOrNull?.id; + + void onPrevious() { + if (!previousAvailable) return; + + final currentIndex = isThreadDetailEnabled + ? availableThreadIds.indexOf(currentThreadId!) + : currentDisplayedEmails + .indexOf(mailboxDashBoardController.selectedEmail.value!); + _navigate(currentIndex - 1, isThreadDetailEnabled); + } + + void _preparePageWithIndex(int index) { + currentMobilePageViewIndex.value = index; + pageController?.dispose(); + pageController = PageController(initialPage: index); + } + + void _goToPageWithEmail(PresentationEmail? presentationEmail) { + mailboxDashBoardController.setSelectedEmail(presentationEmail); + if (PlatformInfo.isWeb && presentationEmail?.routeWeb != null) { + RouteUtils.replaceBrowserHistory( + title: 'Email-${presentationEmail?.id?.id.value ?? ''}', + url: presentationEmail!.routeWeb!); + } + } + + void _navigateToEmail(int emailIndex) { + final email = currentDisplayedEmails[emailIndex]; + _preparePageWithIndex(emailIndex); + _goToPageWithEmail(email); + } + + void _navigateToThread(int threadIndex) { + final threadId = availableThreadIds[threadIndex]; + final email = currentDisplayedEmails.firstWhereOrNull( + (presentationEmail) => presentationEmail.threadId == threadId, + ); + _preparePageWithIndex(threadIndex); + _goToPageWithEmail(email); + } + + void _navigate(int index, bool isThreadDetailEnabled) { + isThreadDetailEnabled ? _navigateToThread(index) : _navigateToEmail(index); + } +} diff --git a/lib/features/thread_detail/presentation/extension/thread_detail_on_selected_email_updated.dart b/lib/features/thread_detail/presentation/extension/thread_detail_on_selected_email_updated.dart index 029d2bd31..b541ef167 100644 --- a/lib/features/thread_detail/presentation/extension/thread_detail_on_selected_email_updated.dart +++ b/lib/features/thread_detail/presentation/extension/thread_detail_on_selected_email_updated.dart @@ -5,6 +5,7 @@ import 'package:tmail_ui_user/features/email/presentation/action/email_ui_action import 'package:tmail_ui_user/features/thread_detail/domain/state/get_emails_by_ids_state.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/domain/usecases/get_thread_by_id_interactor.dart'; +import 'package:tmail_ui_user/features/thread_detail/presentation/action/thread_detail_ui_action.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/thread_detail_controller.dart'; @@ -19,20 +20,43 @@ extension ThreadDetailOnSelectedEmailUpdated on ThreadDetailController { return; } - if (currentExpandedEmailId.value != null) { - mailboxDashBoardController.dispatchEmailUIAction( - DisposePreviousExpandedEmailAction( - currentExpandedEmailId.value!, - ), - ); + scrollController ??= ScrollController(); + + if (currentExpandedEmailId.value == null) { + loadThreadOnThreadChanged = isThreadDetailEnabled; + _preloadSelectedEmail(selectedEmail!); + return; } - _preloadSelectedEmail(selectedEmail!); + // Thread setting updated, no need to dispose current single email controller + if (currentExpandedEmailId.value == selectedEmail!.id) { + _preloadSelectedEmail(selectedEmail); + + if (isThreadDetailEnabled && selectedEmail.threadId != null) { + mailboxDashBoardController.dispatchThreadDetailUIAction( + LoadThreadDetailAfterSelectedEmailAction( + selectedEmail.threadId!, + ) + ); + } + return; + } + + mailboxDashBoardController.dispatchEmailUIAction( + DisposePreviousExpandedEmailAction( + currentExpandedEmailId.value!, + ), + ); + loadThreadOnThreadChanged = isThreadDetailEnabled; + _preloadSelectedEmail(selectedEmail); } void _preloadSelectedEmail(PresentationEmail selectedEmail) { consumeState(Stream.fromIterable([ - Right(PreloadEmailIdsInThreadSuccess([selectedEmail.id!])), + Right(PreloadEmailIdsInThreadSuccess( + [selectedEmail.id!], + threadId: selectedEmail.threadId, + )), Right(PreloadEmailsByIdsSuccess([selectedEmail])), ])); } diff --git a/lib/features/thread_detail/presentation/extension/toggle_thread_detail_collape_expand.dart b/lib/features/thread_detail/presentation/extension/toggle_thread_detail_collape_expand.dart index 9cb5c80fb..f029a3369 100644 --- a/lib/features/thread_detail/presentation/extension/toggle_thread_detail_collape_expand.dart +++ b/lib/features/thread_detail/presentation/extension/toggle_thread_detail_collape_expand.dart @@ -11,6 +11,8 @@ extension ToggleThreadDetailCollapeExpand on ThreadDetailController { if (emailId == null || expansionStatus == null) return; if (expansionStatus == EmailInThreadStatus.expanded) { + if (emailIdsPresentation.length == 1) return; + emailIdsPresentation[emailId] = presentationEmail.copyWith( emailInThreadStatus: EmailInThreadStatus.collapsed, ); @@ -18,6 +20,7 @@ extension ToggleThreadDetailCollapeExpand on ThreadDetailController { CollapseEmailInThreadDetailAction(emailId), ); currentExpandedEmailId.value = null; + threadDetailManager.currentMobilePageViewIndex.refresh(); return; } @@ -42,5 +45,6 @@ extension ToggleThreadDetailCollapeExpand on ThreadDetailController { ); } } + threadDetailManager.currentMobilePageViewIndex.refresh(); } } \ No newline at end of file diff --git a/lib/features/thread_detail/presentation/thread_detail_bindings.dart b/lib/features/thread_detail/presentation/thread_detail_bindings.dart index 2c7cd73c3..688fcfc83 100644 --- a/lib/features/thread_detail/presentation/thread_detail_bindings.dart +++ b/lib/features/thread_detail/presentation/thread_detail_bindings.dart @@ -18,6 +18,7 @@ import 'package:tmail_ui_user/features/thread_detail/domain/usecases/get_emails_ import 'package:tmail_ui_user/features/thread_detail/domain/usecases/get_thread_by_id_interactor.dart'; import 'package:tmail_ui_user/features/thread_detail/domain/usecases/get_thread_detail_status_interactor.dart'; import 'package:tmail_ui_user/features/thread_detail/presentation/thread_detail_controller.dart'; +import 'package:tmail_ui_user/features/thread_detail/presentation/thread_detail_manager.dart'; import 'package:tmail_ui_user/main/exceptions/cache_exception_thrower.dart'; import 'package:tmail_ui_user/main/exceptions/remote_exception_thrower.dart'; @@ -31,8 +32,10 @@ class ThreadDetailBindings extends BaseBindings { @override void bindingsController() { - Get.put(ThreadDetailController( + Get.put(ThreadDetailManager( Get.find(), + )); + Get.put(ThreadDetailController( Get.find(), Get.find(), Get.find(), diff --git a/lib/features/thread_detail/presentation/thread_detail_controller.dart b/lib/features/thread_detail/presentation/thread_detail_controller.dart index e8d38bb9d..fcf09baa9 100644 --- a/lib/features/thread_detail/presentation/thread_detail_controller.dart +++ b/lib/features/thread_detail/presentation/thread_detail_controller.dart @@ -1,3 +1,4 @@ +import 'package:debounce_throttle/debounce_throttle.dart'; import 'package:flutter/material.dart'; import 'dart:async'; @@ -38,20 +39,17 @@ import 'package:tmail_ui_user/features/network_connection/presentation/network_c import 'package:tmail_ui_user/features/search/email/presentation/search_email_controller.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/domain/state/get_emails_by_ids_state.dart'; -import 'package:tmail_ui_user/features/thread_detail/domain/state/get_thread_detail_status_state.dart'; import 'package:tmail_ui_user/features/thread_detail/domain/usecases/get_thread_by_id_interactor.dart'; import 'package:tmail_ui_user/features/thread_detail/domain/usecases/get_emails_by_ids_interactor.dart'; import 'package:tmail_ui_user/features/thread_detail/presentation/action/thread_detail_ui_action.dart'; -import 'package:tmail_ui_user/features/thread_detail/domain/usecases/get_thread_detail_status_interactor.dart'; import 'package:tmail_ui_user/features/thread_detail/presentation/extension/handle_email_moved_action.dart'; import 'package:tmail_ui_user/features/thread_detail/presentation/extension/handle_get_email_ids_by_thread_id_success.dart'; import 'package:tmail_ui_user/features/thread_detail/presentation/extension/handle_get_emails_by_ids_success.dart'; import 'package:tmail_ui_user/features/thread_detail/presentation/extension/handle_get_thread_by_id_failure.dart'; import 'package:tmail_ui_user/features/thread_detail/presentation/extension/handle_refresh_thread_detail_action.dart'; import 'package:tmail_ui_user/features/thread_detail/presentation/extension/initialize_thread_detail_emails.dart'; -import 'package:tmail_ui_user/features/thread_detail/presentation/extension/refresh_thread_detail_on_setting_changed.dart'; import 'package:tmail_ui_user/features/thread_detail/presentation/extension/thread_detail_on_selected_email_updated.dart'; -import 'package:tmail_ui_user/features/thread_detail/presentation/model/thread_detail_setting_status.dart'; +import 'package:tmail_ui_user/features/thread_detail/presentation/thread_detail_manager.dart'; import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; import 'package:tmail_ui_user/main/routes/route_navigation.dart'; import 'package:tmail_ui_user/features/thread_detail/presentation/extension/handle_collapsed_email_download_states.dart'; @@ -60,7 +58,6 @@ import 'package:tmail_ui_user/features/thread_detail/presentation/extension/mark import 'package:tmail_ui_user/features/thread_detail/presentation/extension/quick_create_rule_from_collapsed_email_success.dart'; class ThreadDetailController extends BaseController { - final GetThreadDetailStatusInteractor _getThreadDetailStatusInteractor; final GetThreadByIdInteractor _getEmailIdsByThreadIdInteractor; final GetEmailsByIdsInteractor getEmailsByIdsInteractor; final MarkAsEmailReadInteractor _markAsEmailReadInteractor; @@ -70,7 +67,6 @@ class ThreadDetailController extends BaseController { final DownloadAttachmentForWebInteractor _downloadAttachmentForWebInteractor; ThreadDetailController( - this._getThreadDetailStatusInteractor, this._getEmailIdsByThreadIdInteractor, this.getEmailsByIdsInteractor, this._markAsEmailReadInteractor, @@ -90,18 +86,34 @@ class ThreadDetailController extends BaseController { IndividualHeaderIdentifier.listUnsubscribeHeader.value, }); final cachedEmailLoaded = {}; + late final _threadGetDebouncer = Debouncer( + const Duration(milliseconds: 500), + initialValue: null, + checkEquality: true, + onChanged: (threadId) { + if (_validateLoadThread(threadId)) { + consumeState(_getEmailIdsByThreadIdInteractor.execute( + threadId!, + session!, + accountId!, + sentMailboxId!, + ownEmailAddress!, + selectedEmailId: mailboxDashBoardController.selectedEmail.value?.id, + )); + } + }, + ); final mailboxDashBoardController = Get.find(); final searchEmailController = Get.find(); final networkConnectionController = Get.find(); + final threadDetailManager = Get.find(); final downloadManager = Get.find(); final downloadProgressState = StreamController>(); ScrollController? scrollController; CreateNewEmailRuleFilterInteractor? _createNewEmailRuleFilterInteractor; - AppLifecycleListener? appLifecycleListener; - ThreadDetailSettingStatus threadDetailSettingStatus = ThreadDetailSettingStatus.loading; - bool threadDetailWasEnabled = true; + bool loadThreadOnThreadChanged = false; AccountId? get accountId => mailboxDashBoardController.accountId.value; Session? get session => mailboxDashBoardController.sessionCurrent; @@ -121,21 +133,11 @@ class ThreadDetailController extends BaseController { bool get networkConnected => networkConnectionController.isNetworkConnectionAvailable(); bool get isThreadDetailEnabled => - threadDetailSettingStatus == ThreadDetailSettingStatus.enabled; + threadDetailManager.isThreadDetailEnabled; @override void onInit() { super.onInit(); - consumeState(_getThreadDetailStatusInteractor.execute()); - appLifecycleListener = AppLifecycleListener( - onResume: () { - if (threadDetailSettingStatus == ThreadDetailSettingStatus.loading) { - return; - } - - consumeState(_getThreadDetailStatusInteractor.execute()); - }, - ); ever(mailboxDashBoardController.accountId, (accountId) { if (accountId == null) return; @@ -172,22 +174,10 @@ class ThreadDetailController extends BaseController { }) ..listUnsubscribeHeader?.clear(); } - } else if (action is UpdatedThreadDetailSettingAction) { - consumeState(_getThreadDetailStatusInteractor.execute()); } else if (action is EmailMovedAction) { handleEmailMovedAction(action); } else if (action is LoadThreadDetailAfterSelectedEmailAction) { - if (_validateLoadThread(action)) { - scrollController = ScrollController(); - consumeState(_getEmailIdsByThreadIdInteractor.execute( - action.threadId, - session!, - accountId!, - sentMailboxId!, - ownEmailAddress!, - selectedEmailId: mailboxDashBoardController.selectedEmail.value?.id, - )); - } + _threadGetDebouncer.value = action.threadId; } // Reset [threadDetailUIAction] to original value mailboxDashBoardController.dispatchThreadDetailUIAction( @@ -205,9 +195,9 @@ class ThreadDetailController extends BaseController { }); } - bool _validateLoadThread(LoadThreadDetailAfterSelectedEmailAction action) { + bool _validateLoadThread(ThreadId? threadId) { return mailboxDashBoardController.selectedEmail.value?.threadId != null && - action.threadId == mailboxDashBoardController.selectedEmail.value?.threadId && + threadId == mailboxDashBoardController.selectedEmail.value?.threadId && session != null && accountId != null && sentMailboxId != null && @@ -223,6 +213,7 @@ class ThreadDetailController extends BaseController { currentExpandedEmailId.value = null; currentEmailLoaded.value = null; cachedEmailLoaded.clear(); + _threadGetDebouncer.value = null; } @override @@ -240,13 +231,6 @@ class ThreadDetailController extends BaseController { quickCreateRuleFromCollapsedEmailSuccess(success); } else if (success is DownloadAttachmentForWebSuccess) { handleDownloadSuccess(success); - } else if (success is GetThreadDetailStatusSuccess) { - threadDetailSettingStatus = success.threadDetailEnabled - ? ThreadDetailSettingStatus.enabled - : ThreadDetailSettingStatus.disabled; - refreshThreadDetailOnSettingChanged(); - } else if (success is GettingThreadDetailStatus) { - threadDetailSettingStatus = ThreadDetailSettingStatus.loading; } else { super.handleSuccessViewState(success); } @@ -266,10 +250,6 @@ class ThreadDetailController extends BaseController { handleDownloadFailure(failure); return; } - if (failure is GetThreadDetailStatusFailure) { - threadDetailSettingStatus = ThreadDetailSettingStatus.enabled; - refreshThreadDetailOnSettingChanged(); - } if (failure is PrintEmailFailure) { if (currentOverlayContext != null && currentContext != null) { appToast.showToastErrorMessage( diff --git a/lib/features/thread_detail/presentation/thread_detail_manager.dart b/lib/features/thread_detail/presentation/thread_detail_manager.dart new file mode 100644 index 000000000..6b0a3393f --- /dev/null +++ b/lib/features/thread_detail/presentation/thread_detail_manager.dart @@ -0,0 +1,121 @@ +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; +import 'package:core/utils/platform_info.dart'; +import 'package:flutter/widgets.dart'; +import 'package:get/get_instance/get_instance.dart'; +import 'package:get/state_manager.dart'; +import 'package:jmap_dart_client/jmap/mail/email/email.dart'; +import 'package:model/email/presentation_email.dart'; +import 'package:tmail_ui_user/features/base/base_controller.dart'; +import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart'; +import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/model/dashboard_routes.dart'; +import 'package:tmail_ui_user/features/thread_detail/domain/state/get_thread_detail_status_state.dart'; +import 'package:tmail_ui_user/features/thread_detail/domain/usecases/get_thread_detail_status_interactor.dart'; +import 'package:tmail_ui_user/features/thread_detail/presentation/action/thread_detail_ui_action.dart'; +import 'package:tmail_ui_user/features/thread_detail/presentation/extension/initialize_thread_detail_manager.dart'; +import 'package:tmail_ui_user/features/thread_detail/presentation/extension/refresh_thread_detail_on_setting_changed.dart'; +import 'package:tmail_ui_user/features/thread_detail/presentation/model/thread_detail_setting_status.dart'; + +class ThreadDetailManager extends BaseController { + final mailboxDashBoardController = Get.find(); + + final GetThreadDetailStatusInteractor _getThreadDetailStatusInteractor; + + ThreadDetailManager(this._getThreadDetailStatusInteractor); + + final availableThreadIds = RxList(); + final currentMobilePageViewIndex = 0.obs; + PageController? pageController; + + final threadDetailSettingStatus = ThreadDetailSettingStatus.loading.obs; + AppLifecycleListener? appLifecycleListener; + bool threadDetailWasEnabled = true; + + bool get isSearchingOnMobile => + mailboxDashBoardController.searchController.isSearchEmailRunning && + PlatformInfo.isMobile; + + RxList get currentDisplayedEmails => isSearchingOnMobile + ? mailboxDashBoardController.listResultSearch + : mailboxDashBoardController.emailsInCurrentMailbox; + + ThreadId? get currentThreadId => + mailboxDashBoardController.selectedEmail.value?.threadId; + EmailId? get currentEmailId => + mailboxDashBoardController.selectedEmail.value?.id; + + bool get isThreadDetailEnabled => + threadDetailSettingStatus.value == ThreadDetailSettingStatus.enabled; + + @override + void onInit() { + super.onInit(); + consumeState(_getThreadDetailStatusInteractor.execute()); + appLifecycleListener = AppLifecycleListener( + onResume: () { + if (threadDetailSettingStatus.value == ThreadDetailSettingStatus.loading) { + return; + } + + consumeState(_getThreadDetailStatusInteractor.execute()); + }, + ); + ever(mailboxDashBoardController.threadDetailUIAction, (action) { + if (action is UpdatedThreadDetailSettingAction) { + consumeState(_getThreadDetailStatusInteractor.execute()); + } + // Reset [threadDetailUIAction] to original value + mailboxDashBoardController.dispatchThreadDetailUIAction( + ThreadDetailUIAction(), + ); + }); + ever( + currentDisplayedEmails, + (emails) { + if (mailboxDashBoardController.dashboardRoute.value != DashboardRoutes.threadDetailed) { + return; + } + initializeThreadDetailManager(emails); + }, + ); + ever( + mailboxDashBoardController.dashboardRoute, + (route) { + final selectedEmail = mailboxDashBoardController.selectedEmail.value; + if (route != DashboardRoutes.threadDetailed || selectedEmail == null) { + currentMobilePageViewIndex.value = -1; + pageController?.dispose(); + pageController = null; + availableThreadIds.clear(); + return; + } + + initializeThreadDetailManager(currentDisplayedEmails); + }, + ); + } + + @override + void handleSuccessViewState(Success success) { + if (success is GetThreadDetailStatusSuccess) { + threadDetailSettingStatus.value = success.threadDetailEnabled + ? ThreadDetailSettingStatus.enabled + : ThreadDetailSettingStatus.disabled; + refreshThreadDetailOnSettingChanged(); + } else if (success is GettingThreadDetailStatus) { + threadDetailSettingStatus.value = ThreadDetailSettingStatus.loading; + } else { + super.handleSuccessViewState(success); + } + } + + @override + void handleFailureViewState(Failure failure) { + if (failure is GetThreadDetailStatusFailure) { + threadDetailSettingStatus.value = ThreadDetailSettingStatus.enabled; + refreshThreadDetailOnSettingChanged(); + } else { + super.handleFailureViewState(failure); + } + } +} diff --git a/lib/features/thread_detail/presentation/thread_detail_view.dart b/lib/features/thread_detail/presentation/thread_detail_view.dart index 38835edac..263aaba32 100644 --- a/lib/features/thread_detail/presentation/thread_detail_view.dart +++ b/lib/features/thread_detail/presentation/thread_detail_view.dart @@ -2,37 +2,37 @@ import 'package:collection/collection.dart'; import 'package:core/presentation/extensions/color_extension.dart'; import 'package:core/presentation/state/failure.dart'; import 'package:core/presentation/state/success.dart'; +import 'package:core/presentation/views/button/tmail_button_widget.dart'; +import 'package:core/utils/platform_info.dart'; import 'package:dartz/dartz.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; -import 'package:model/email/email_in_thread_status.dart'; import 'package:model/extensions/session_extension.dart'; import 'package:model/email/email_action_type.dart'; 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/email_view.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/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'; -import 'package:tmail_ui_user/features/thread_detail/presentation/extension/get_thread_detail_loading_view.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_page_changed.dart'; import 'package:tmail_ui_user/features/thread_detail/presentation/extension/thread_detail_on_email_action_click.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/main/localizations/app_localizations.dart'; class ThreadDetailView extends GetWidget { const ThreadDetailView({super.key}); @override Widget build(BuildContext context) { - final multipleEmailsView = SelectionArea(child: SafeArea( + final child = SelectionArea(child: SafeArea( child: Column( children: [ Obx(() { - if (showLoadingView(controller.viewState.value)) { - return const SizedBox.shrink(); - } + final isLoading = showLoadingView(controller.viewState.value); return ThreadDetailAppBar( responsiveUtils: controller.responsiveUtils, @@ -42,39 +42,84 @@ class ThreadDetailView extends GetWidget { lastEmailOfThread: controller.emailIdsPresentation.values.lastOrNull, ownUserName: controller.session?.getOwnEmailAddress() ?? '', mailboxContain: _getMailboxContain(), - onEmailActionClick: controller.threadDetailOnEmailActionClick, - onMoreActionClick: (presentationEmail, position) => controller.emailActionReactor.handleMoreEmailAction( - mailboxContain: controller.getThreadDetailEmailMailboxContains(presentationEmail), - presentationEmail: presentationEmail, - position: position, - responsiveUtils: controller.responsiveUtils, - imagePaths: controller.imagePaths, - username: controller.session?.username, - handleEmailAction: controller.threadDetailOnEmailActionClick, - additionalActions: [ - if (controller.responsiveUtils.isMobile(context)) ...[ - EmailActionType.forward, - EmailActionType.replyAll, - EmailActionType.replyToList, - ], - EmailActionType.markAsStarred, - EmailActionType.unMarkAsStarred, - EmailActionType.moveToTrash, - EmailActionType.deletePermanently, - EmailActionType.printAll, - EmailActionType.moveToMailbox, - ], - emailIsRead: presentationEmail.hasRead, - ), + onEmailActionClick: isLoading + ? null + : controller.threadDetailOnEmailActionClick, + onMoreActionClick: (presentationEmail, position) => isLoading + ? null + : controller.emailActionReactor.handleMoreEmailAction( + mailboxContain: controller.getThreadDetailEmailMailboxContains( + presentationEmail, + ), + presentationEmail: presentationEmail, + position: position, + responsiveUtils: controller.responsiveUtils, + imagePaths: controller.imagePaths, + username: controller.session?.username, + handleEmailAction: controller.threadDetailOnEmailActionClick, + additionalActions: [ + if (controller.responsiveUtils.isMobile(context)) ...[ + EmailActionType.forward, + EmailActionType.replyAll, + EmailActionType.replyToList, + ], + EmailActionType.markAsStarred, + EmailActionType.unMarkAsStarred, + EmailActionType.moveToTrash, + EmailActionType.deletePermanently, + EmailActionType.printAll, + EmailActionType.moveToMailbox, + ], + emailIsRead: presentationEmail.hasRead, + ), + optionWidgets: [ + if (controller.previousAvailable) + TMailButtonWidget.fromIcon( + icon: controller.imagePaths.icNewer, + iconSize: EmailViewAppBarWidgetStyles.buttonIconSize, + iconColor: EmailViewAppBarWidgetStyles.iconColor, + tooltipMessage: AppLocalizations.of(context).newer, + backgroundColor: Colors.transparent, + onTapActionCallback: controller.onPrevious, + ), + if (controller.nextAvailable) + TMailButtonWidget.fromIcon( + icon: controller.imagePaths.icOlder, + iconSize: EmailViewAppBarWidgetStyles.buttonIconSize, + iconColor: EmailViewAppBarWidgetStyles.iconColor, + tooltipMessage: AppLocalizations.of(context).older, + backgroundColor: Colors.transparent, + onTapActionCallback: controller.onNext, + ), + ], ); }), - Obx(() => controller.getThreadDetailLoadingView( - isResponsiveDesktop: controller.responsiveUtils.isDesktop(context), - isLoading: showLoadingView(controller.viewState.value), - )), Obx(() { - if (showLoadingView(controller.viewState.value)) { - return const SizedBox.shrink(); + if (PlatformInfo.isMobile) { + final manager = controller.threadDetailManager; + final currentIndex = manager.currentMobilePageViewIndex.value; + + return Expanded( + child: PageView.builder( + controller: manager.pageController, + itemCount: manager.isThreadDetailEnabled + ? manager.availableThreadIds.length + : manager.currentDisplayedEmails.length, + itemBuilder: (context, index) { + if (index != currentIndex) { + return const SizedBox.shrink(); + } + + return SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + children: controller.getThreadDetailEmailViews() + ), + ); + }, + onPageChanged: controller.onThreadPageChanged, + ), + ); } return Expanded( @@ -98,28 +143,18 @@ class ThreadDetailView extends GetWidget { ); }), Obx(() { - if (showLoadingView(controller.viewState.value)) { - return const SizedBox.shrink(); - } - final expandedEmailId = controller.currentExpandedEmailId.value; if (expandedEmailId == null) { - return _roundBottomPlaceHolder( - isDesktop: controller.responsiveUtils.isDesktop(context), - ); + return const SizedBox.shrink(); } final expandedPresentationEmail = controller.emailIdsPresentation[expandedEmailId]; if (expandedPresentationEmail == null) { - return _roundBottomPlaceHolder( - isDesktop: controller.responsiveUtils.isDesktop(context), - ); + return const SizedBox.shrink(); } final currentEmailLoaded = controller.currentEmailLoaded.value; if (currentEmailLoaded == null) { - return _roundBottomPlaceHolder( - isDesktop: controller.responsiveUtils.isDesktop(context), - ); + return const SizedBox.shrink(); } return Padding( @@ -162,29 +197,10 @@ class ThreadDetailView extends GetWidget { ), )); - return Obx(() { - if (showLoadingView(controller.viewState.value) && - controller.emailIdsPresentation.isEmpty && - controller.responsiveUtils.isTabletLarge(context)) { - return controller.getThreadDetailLoadingView( - isResponsiveDesktop: false, - isLoading: true, - isExpanded: false, - ); - } - - if (controller.emailIdsPresentation.length == 1 && - controller.emailIdsPresentation.values.firstOrNull?.emailInThreadStatus == EmailInThreadStatus.expanded) { - final emailId = controller.emailIdsPresentation.values.firstOrNull!.id; - - return EmailView( - key: ValueKey(emailId?.id.value), - emailId: emailId, - ); - } - - return multipleEmailsView; - }); + return ClipRRect( + borderRadius: const BorderRadius.vertical(bottom: Radius.circular(20)), + child: child, + ); } EdgeInsetsGeometry _padding(BuildContext context) { @@ -202,21 +218,6 @@ class ThreadDetailView extends GetWidget { return controller.mailboxDashBoardController.selectedMailbox.value; } - Widget _roundBottomPlaceHolder({required bool isDesktop}) { - return Container( - height: 40, - margin: isDesktop - ? const EdgeInsetsDirectional.only(end: 16) - : EdgeInsets.zero, - decoration: const BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.vertical( - bottom: Radius.circular(20), - ), - ) - ); - } - bool showLoadingView(Either viewState) { final viewStateValue = viewState.fold(id, id); return viewStateValue is GettingThreadById && 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 90996957e..3b334be37 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 @@ -139,21 +139,24 @@ class ThreadDetailAppBar extends StatelessWidget { ), ], if (!responsiveUtils.isMobile(context)) const Spacer(), - ...optionWidgets, + ] else const Spacer(), + ...optionWidgets, + if (lastEmailOfThread != null) TMailButtonWidget.fromIcon( icon: imagePaths.icMoreVertical, iconSize: EmailViewAppBarWidgetStyles.buttonIconSize, iconColor: EmailViewAppBarWidgetStyles.iconColor, backgroundColor: Colors.transparent, tooltipMessage: AppLocalizations.of(context).more, - onTapActionCallback: responsiveUtils.isScreenWithShortestSide(context) + onTapActionCallback: responsiveUtils.isScreenWithShortestSide(context) && + lastEmailOfThread != null ? () => onMoreActionClick?.call(lastEmailOfThread!, null) : null, - onTapActionAtPositionCallback: !responsiveUtils.isScreenWithShortestSide(context) + onTapActionAtPositionCallback: !responsiveUtils.isScreenWithShortestSide(context) && + lastEmailOfThread != null ? (position) => onMoreActionClick?.call(lastEmailOfThread!, position) : null, ), - ] else const Spacer(), ], ), ); diff --git a/test/features/thread_detail/presentation/extension/handle_get_email_ids_by_thread_id_success_test.dart b/test/features/thread_detail/presentation/extension/handle_get_email_ids_by_thread_id_success_test.dart index a4939fc85..838f71c7b 100644 --- a/test/features/thread_detail/presentation/extension/handle_get_email_ids_by_thread_id_success_test.dart +++ b/test/features/thread_detail/presentation/extension/handle_get_email_ids_by_thread_id_success_test.dart @@ -31,10 +31,14 @@ void main() { }; when(threadDetailController.emailIdsPresentation) .thenReturn(initialEmailIdsPresentation.obs); + when(threadDetailController.mailboxDashBoardController) + .thenReturn(mailboxDashBoardController); + when(mailboxDashBoardController.selectedEmail) + .thenReturn(Rxn(PresentationEmail(id: EmailId(Id('1'))))); // act threadDetailController.handleGetEmailIdsByThreadIdSuccess( - GetThreadByIdSuccess([]), + GetThreadByIdSuccess([], threadId: null), ); // assert @@ -58,6 +62,10 @@ void main() { }; when(threadDetailController.emailIdsPresentation) .thenReturn(initialEmailIdsPresentation.obs); + when(threadDetailController.mailboxDashBoardController) + .thenReturn(mailboxDashBoardController); + when(mailboxDashBoardController.selectedEmail) + .thenReturn(Rxn(PresentationEmail(id: EmailId(Id('1'))))); // act threadDetailController.handleGetEmailIdsByThreadIdSuccess( @@ -67,6 +75,7 @@ void main() { EmailId(Id('3')), EmailId(Id('4')), ], + threadId: null, updateCurrentThreadDetail: true, ), ); @@ -113,6 +122,7 @@ void main() { EmailId(Id('3')), EmailId(Id('4')), ], + threadId: null, updateCurrentThreadDetail: false, ), ); @@ -156,6 +166,7 @@ void main() { EmailId(Id('3')), EmailId(Id('4')), ], + threadId: null, updateCurrentThreadDetail: false, ), ); diff --git a/test/features/thread_detail/presentation/extension/initialize_thread_detail_emails_test.dart b/test/features/thread_detail/presentation/extension/initialize_thread_detail_emails_test.dart index c7f686afd..c33fcb2fc 100644 --- a/test/features/thread_detail/presentation/extension/initialize_thread_detail_emails_test.dart +++ b/test/features/thread_detail/presentation/extension/initialize_thread_detail_emails_test.dart @@ -69,7 +69,7 @@ void main() { // Act threadDetailController.initializeThreadDetailEmails( - GetThreadByIdSuccess(emailIds), + GetThreadByIdSuccess(emailIds, threadId: null), ); // Assert diff --git a/test/features/thread_detail/presentation/extension/thread_detail_on_selected_email_updated_test.dart b/test/features/thread_detail/presentation/extension/thread_detail_on_selected_email_updated_test.dart index 1f6def54b..be086a2f8 100644 --- a/test/features/thread_detail/presentation/extension/thread_detail_on_selected_email_updated_test.dart +++ b/test/features/thread_detail/presentation/extension/thread_detail_on_selected_email_updated_test.dart @@ -85,7 +85,7 @@ void main() { expect( streamsConsumed, emitsInOrder([ - Right(PreloadEmailIdsInThreadSuccess([selectedEmail.id!])), + Right(PreloadEmailIdsInThreadSuccess([selectedEmail.id!], threadId: ThreadId(Id('1')))), Right(PreloadEmailsByIdsSuccess([selectedEmail])), ]), );