From af755ae6c683f56776cf205b69fec874fd09c061 Mon Sep 17 00:00:00 2001 From: dab246 Date: Thu, 18 Jan 2024 20:12:30 +0700 Subject: [PATCH] TF-2362 Add `Load more` button at bottom emails list Signed-off-by: dab246 (cherry picked from commit 9563f13e47f33e4ee5e351bdc108b82c160f1010) --- .../domain/constants/thread_constants.dart | 3 +- .../presentation/thread_controller.dart | 71 +++++++++------ .../thread/presentation/thread_view.dart | 87 ++++++++++++++----- 3 files changed, 114 insertions(+), 47 deletions(-) diff --git a/lib/features/thread/domain/constants/thread_constants.dart b/lib/features/thread/domain/constants/thread_constants.dart index 72c0957f7..6095dea2e 100644 --- a/lib/features/thread/domain/constants/thread_constants.dart +++ b/lib/features/thread/domain/constants/thread_constants.dart @@ -4,7 +4,8 @@ import 'package:jmap_dart_client/jmap/mail/email/individual_header_identifier.da import 'package:model/email/email_property.dart'; class ThreadConstants { - static final defaultLimit = UnsignedInt(20); + static const maxCountEmails = 20; + static final defaultLimit = UnsignedInt(maxCountEmails); static final propertiesDefault = Properties({ EmailProperty.id, EmailProperty.subject, diff --git a/lib/features/thread/presentation/thread_controller.dart b/lib/features/thread/presentation/thread_controller.dart index 6e4274cd7..fa7bc986d 100644 --- a/lib/features/thread/presentation/thread_controller.dart +++ b/lib/features/thread/presentation/thread_controller.dart @@ -4,6 +4,7 @@ import 'package:core/utils/app_logger.dart'; import 'package:core/utils/platform_info.dart'; import 'package:dartz/dartz.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/scheduler.dart'; import 'package:flutter/services.dart'; import 'package:get/get.dart'; import 'package:jmap_dart_client/jmap/account_id.dart'; @@ -97,10 +98,10 @@ class ThreadController extends BaseController with EmailActionController { final listEmailDrag = [].obs; bool _rangeSelectionMode = false; final openingEmail = RxBool(false); + final loadingMoreStatus = Rx(LoadingMoreStatus.idle); - bool canLoadMore = true; - bool canSearchMore = true; - LoadingMoreStatus loadingMoreStatus = LoadingMoreStatus.idle; + bool canLoadMore = false; + bool canSearchMore = false; MailboxId? _currentMailboxId; jmap.State? _currentEmailState; final ScrollController listEmailController = ScrollController(); @@ -161,7 +162,7 @@ class ThreadController extends BaseController with EmailActionController { } else if (success is SearchMoreEmailSuccess) { _searchMoreEmailsSuccess(success); } else if (success is SearchingMoreState || success is LoadingMoreEmails) { - loadingMoreStatus = LoadingMoreStatus.running; + loadingMoreStatus.value = LoadingMoreStatus.running; } else if (success is GetEmailByIdLoading) { openingEmail.value = true; } else if (success is GetEmailByIdSuccess) { @@ -190,13 +191,18 @@ class ThreadController extends BaseController with EmailActionController { mailboxDashBoardController.refreshingMailboxState.value = Left(failure); canSearchMore = false; mailboxDashBoardController.emailsInCurrentMailbox.clear(); - } else if (failure is SearchMoreEmailFailure || failure is LoadMoreEmailsFailure) { - loadingMoreStatus = LoadingMoreStatus.completed; + } else if (failure is SearchMoreEmailFailure) { + loadingMoreStatus.value = LoadingMoreStatus.completed; + canSearchMore = true; + } else if (failure is LoadMoreEmailsFailure) { + loadingMoreStatus.value = LoadingMoreStatus.completed; + canLoadMore = true; } else if (failure is GetEmailByIdFailure) { openingEmail.value = false; popAndPush(AppRoutes.unknownRoutePage); } else if (failure is GetAllEmailFailure) { mailboxDashBoardController.refreshingMailboxState.value = Left(failure); + canLoadMore = true; } } @@ -217,8 +223,8 @@ class ThreadController extends BaseController with EmailActionController { } void _resetLoadingMore() { - if (loadingMoreStatus == LoadingMoreStatus.running) { - loadingMoreStatus = LoadingMoreStatus.idle; + if (loadingMoreStatus.value == LoadingMoreStatus.running) { + loadingMoreStatus.value = LoadingMoreStatus.idle; } } @@ -386,15 +392,15 @@ class ThreadController extends BaseController with EmailActionController { void _resetToOriginalValue() { dispatchState(Right(LoadingState())); mailboxDashBoardController.emailsInCurrentMailbox.clear(); - canLoadMore = true; - loadingMoreStatus = LoadingMoreStatus.idle; + canLoadMore = false; + loadingMoreStatus.value = LoadingMoreStatus.idle; cancelSelectEmail(); } void _getAllEmailSuccess(GetAllEmailSuccess success) { mailboxDashBoardController.refreshingMailboxState.value = Right(success); _currentEmailState = success.currentEmailState; - log('ThreadController::_getAllEmailSuccess():_currentEmailState: $_currentEmailState'); + log('ThreadController::_getAllEmailSuccess():COUNT = ${success.emailList.length} | EMAIL_STATE = $_currentEmailState'); final newListEmail = success.emailList.syncPresentationEmail( mapMailboxById: mailboxDashBoardController.mapMailboxById, selectedMailbox: currentMailbox, @@ -402,14 +408,22 @@ class ThreadController extends BaseController with EmailActionController { isSearchEmailRunning: searchController.isSearchEmailRunning ); mailboxDashBoardController.updateEmailList(newListEmail); - if (listEmailController.hasClients) { - listEmailController.animateTo(0, duration: const Duration(milliseconds: 500), curve: Curves.fastOutSlowIn); - } + + canLoadMore = newListEmail.length >= ThreadConstants.maxCountEmails; + + SchedulerBinding.instance.addPostFrameCallback((_) { + if (listEmailController.hasClients) { + listEmailController.animateTo( + 0, + duration: const Duration(milliseconds: 500), + curve: Curves.fastOutSlowIn); + } + }); } void _refreshChangesAllEmailSuccess(RefreshChangesAllEmailSuccess success) { _currentEmailState = success.currentEmailState; - + log('ThreadController::_refreshChangesAllEmailSuccess: COUNT = ${success.emailList.length}'); final emailsBeforeChanges = mailboxDashBoardController.emailsInCurrentMailbox; final emailsAfterChanges = success.emailList; final newListEmail = emailsAfterChanges.combine(emailsBeforeChanges); @@ -421,6 +435,8 @@ class ThreadController extends BaseController with EmailActionController { ); mailboxDashBoardController.updateEmailList(emailListSynced); + canLoadMore = newListEmail.length >= ThreadConstants.maxCountEmails; + if (mailboxDashBoardController.emailsInCurrentMailbox.isEmpty) { refreshAllEmail(); } @@ -474,8 +490,8 @@ class ThreadController extends BaseController with EmailActionController { void refreshAllEmail() { dispatchState(Right(LoadingState())); - canLoadMore = true; - loadingMoreStatus == LoadingMoreStatus.idle; + canLoadMore = false; + loadingMoreStatus.value == LoadingMoreStatus.idle; cancelSelectEmail(); if (searchController.isSearchEmailRunning) { @@ -551,7 +567,7 @@ class ThreadController extends BaseController with EmailActionController { } void _loadMoreEmailsSuccess(LoadMoreEmailsSuccess success) { - loadingMoreStatus = LoadingMoreStatus.completed; + log('ThreadController::_loadMoreEmailsSuccess: COUNT = ${success.emailList.length}'); if (success.emailList.isNotEmpty) { final appendableList = success.emailList .where(_belongToCurrentMailboxId) @@ -565,9 +581,11 @@ class ThreadController extends BaseController with EmailActionController { ); mailboxDashBoardController.emailsInCurrentMailbox.addAll(appendableList); + canLoadMore = true; } else { canLoadMore = false; } + loadingMoreStatus.value = LoadingMoreStatus.completed; } SelectMode getSelectMode(PresentationEmail presentationEmail, PresentationEmail? selectedEmail) { @@ -704,7 +722,7 @@ class ThreadController extends BaseController with EmailActionController { } } - bool isSearchActive() => searchController.isSearchEmailRunning; + bool get isSearchActive => searchController.isSearchEmailRunning; bool get isAllSearchInActive => !searchController.isSearchActive() && searchController.isAdvancedSearchViewOpen.isFalse; @@ -719,7 +737,7 @@ class ThreadController extends BaseController with EmailActionController { listEmailController.jumpTo(0); } mailboxDashBoardController.emailsInCurrentMailbox.clear(); - canSearchMore = true; + canSearchMore = false; if (searchController.sortOrderFiltered.value.isScrollByPosition()) { searchController.updateFilterEmail( @@ -770,7 +788,7 @@ class ThreadController extends BaseController with EmailActionController { void _searchEmailsSuccess(SearchEmailSuccess success) { mailboxDashBoardController.refreshingMailboxState.value = Right(success); - canSearchMore = true; + log('ThreadController::_searchEmailsSuccess: COUNT = ${success.emailList.length}'); final resultEmailSearchList = success.emailList .map((email) => email.toSearchPresentationEmail(mailboxDashBoardController.mapMailboxById)) .toList(); @@ -785,9 +803,12 @@ class ThreadController extends BaseController with EmailActionController { isSearchEmailRunning: searchController.isSearchEmailRunning ); mailboxDashBoardController.updateEmailList(newEmailListSynced); + + canSearchMore = newEmailListSynced.length >= ThreadConstants.maxCountEmails; } void searchMoreEmails() { + log('ThreadController::searchMoreEmails:'); if (canSearchMore && _session != null && _accountId != null) { final lastEmail = mailboxDashBoardController.emailsInCurrentMailbox.isNotEmpty ? mailboxDashBoardController.emailsInCurrentMailbox.last @@ -823,10 +844,8 @@ class ThreadController extends BaseController with EmailActionController { } void _searchMoreEmailsSuccess(SearchMoreEmailSuccess success) { - loadingMoreStatus = LoadingMoreStatus.completed; - + log('ThreadController::_searchMoreEmailsSuccess: COUNT = ${success.emailList.length}'); if (success.emailList.isNotEmpty) { - canSearchMore = true; final resultEmailSearchList = success.emailList .map((email) => email.toSearchPresentationEmail(mailboxDashBoardController.mapMailboxById)) .where((email) => mailboxDashBoardController.emailsInCurrentMailbox.every((emailInCurrentMailbox) => emailInCurrentMailbox.id != email.id)) @@ -838,9 +857,9 @@ class ThreadController extends BaseController with EmailActionController { isSearchEmailRunning: searchController.isSearchEmailRunning ); mailboxDashBoardController.emailsInCurrentMailbox.addAll(resultEmailSearchList); - } else { - canSearchMore = false; } + canSearchMore = success.emailList.isNotEmpty; + loadingMoreStatus.value = LoadingMoreStatus.completed; } bool isSelectionEnabled() => mailboxDashBoardController.isSelectionEnabled(); diff --git a/lib/features/thread/presentation/thread_view.dart b/lib/features/thread/presentation/thread_view.dart index 547dec046..47f462039 100644 --- a/lib/features/thread/presentation/thread_view.dart +++ b/lib/features/thread/presentation/thread_view.dart @@ -21,6 +21,7 @@ import 'package:tmail_ui_user/features/quotas/presentation/widget/quotas_banner_ import 'package:tmail_ui_user/features/thread/domain/model/filter_message_option.dart'; import 'package:tmail_ui_user/features/thread/domain/state/search_email_state.dart'; import 'package:tmail_ui_user/features/thread/presentation/model/delete_action_type.dart'; +import 'package:tmail_ui_user/features/thread/presentation/model/loading_more_status.dart'; import 'package:tmail_ui_user/features/thread/presentation/styles/banner_delete_all_spam_emails_styles.dart'; import 'package:tmail_ui_user/features/thread/presentation/styles/banner_empty_trash_styles.dart'; import 'package:tmail_ui_user/features/thread/presentation/styles/item_email_tile_styles.dart'; @@ -251,7 +252,7 @@ class ThreadView extends GetWidget Widget _buildListButtonSelectionForMobile(BuildContext context) { return Obx(() { if ((PlatformInfo.isMobile || (PlatformInfo.isWeb && controller.isSelectionEnabled() - && controller.isSearchActive() && !controller.responsiveUtils.isDesktop(context))) + && controller.isSearchActive && !controller.responsiveUtils.isDesktop(context))) && controller.mailboxDashBoardController.emailsInCurrentMailbox.listEmailSelected.isNotEmpty) { return BottomBarThreadSelectionWidget( controller.imagePaths, @@ -348,26 +349,23 @@ class ThreadView extends GetWidget Widget _buildListEmailBody(BuildContext context, List listPresentationEmail) { return NotificationListener( - onNotification: (ScrollNotification scrollInfo) { - if (scrollInfo is ScrollEndNotification - && !controller.loadingMoreStatus.isRunning - && scrollInfo.metrics.pixels == scrollInfo.metrics.maxScrollExtent - ) { - if (controller.isSearchActive() || controller.searchController.advancedSearchIsActivated.isTrue) { - controller.searchMoreEmails(); - } else { - controller.loadMoreEmails(); - } - } - return false; - }, + onNotification: _handleScrollNotificationListener, child: PlatformInfo.isMobile ? ListView.separated( key: const PageStorageKey('list_presentation_email_in_threads'), controller: controller.listEmailController, physics: const AlwaysScrollableScrollPhysics(), - itemCount: listPresentationEmail.length, - itemBuilder: (context, index) => Obx(() => _buildEmailItemNotDraggable(context, listPresentationEmail[index])), + itemCount: listPresentationEmail.length + 1, + itemBuilder: (context, index) => Obx(() { + if (index == listPresentationEmail.length) { + return _buildLoadMoreButton( + context, + controller.loadingMoreStatus.value); + } + return _buildEmailItemNotDraggable( + context, + listPresentationEmail[index]); + }), separatorBuilder: (context, index) { if (index < listPresentationEmail.length - 1) { return Padding( @@ -388,8 +386,17 @@ class ThreadView extends GetWidget key: const PageStorageKey('list_presentation_email_in_threads'), controller: controller.listEmailController, physics: const AlwaysScrollableScrollPhysics(), - itemCount: listPresentationEmail.length, - itemBuilder: (context, index) => Obx(() => _buildEmailItem(context, listPresentationEmail[index])), + itemCount: listPresentationEmail.length + 1, + itemBuilder: (context, index) => Obx(() { + if (index == listPresentationEmail.length) { + return _buildLoadMoreButton( + context, + controller.loadingMoreStatus.value); + } + return _buildEmailItem( + context, + listPresentationEmail[index]); + }), separatorBuilder: (context, index) { return Padding( padding: ItemEmailTileStyles.getPaddingDividerWeb(context, controller.responsiveUtils), @@ -407,6 +414,46 @@ class ThreadView extends GetWidget ); } + bool _handleScrollNotificationListener(ScrollNotification scrollInfo) { + if (scrollInfo is ScrollEndNotification && + scrollInfo.metrics.pixels == scrollInfo.metrics.maxScrollExtent && + !controller.loadingMoreStatus.value.isRunning + ) { + _handleLoadMoreEmailsRequest(); + } + return false; + } + + void _handleLoadMoreEmailsRequest() { + if (controller.isSearchActive) { + controller.searchMoreEmails(); + } else { + controller.loadMoreEmails(); + } + } + + Widget _buildLoadMoreButton(BuildContext context, LoadingMoreStatus loadingMoreStatus) { + if (((controller.canLoadMore && !controller.isSearchActive) || + (controller.canSearchMore && controller.isSearchActive)) && + !loadingMoreStatus.isRunning) { + return Center( + child: OutlinedButton( + style: OutlinedButton.styleFrom( + backgroundColor: Theme.of(context).scaffoldBackgroundColor, + ), + onPressed: _handleLoadMoreEmailsRequest, + child: Text( + AppLocalizations.of(context).loadMore, + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: Colors.black + ) + ), + ), + ); + } + return const SizedBox.shrink(); + } + Widget _buildEmailItem(BuildContext context, PresentationEmail presentationEmail) { if (controller.responsiveUtils.isWebDesktop(context)) { return _buildEmailItemDraggable(context, presentationEmail); @@ -630,7 +677,7 @@ class ThreadView extends GetWidget } String _getMessageEmptyEmail(BuildContext context) { - if (controller.isSearchActive()) { + if (controller.isSearchActive) { return AppLocalizations.of(context).no_emails_matching_your_search; } else { if (controller.mailboxDashBoardController.filterMessageOption.value == FilterMessageOption.all && @@ -643,7 +690,7 @@ class ThreadView extends GetWidget } String? _getSubMessageEmptyEmail(BuildContext context) { - if (!controller.isSearchActive() + if (!controller.isSearchActive && controller.mailboxDashBoardController.filterMessageOption.value != FilterMessageOption.all) { return AppLocalizations.of(context).reduceSomeFiltersAndTryAgain; } else if (controller.mailboxDashBoardController.filterMessageOption.value == FilterMessageOption.all &&