diff --git a/lib/features/mailbox/presentation/mailbox_controller.dart b/lib/features/mailbox/presentation/mailbox_controller.dart index a74ee5ad9..25dd44f86 100644 --- a/lib/features/mailbox/presentation/mailbox_controller.dart +++ b/lib/features/mailbox/presentation/mailbox_controller.dart @@ -594,7 +594,9 @@ class MailboxController extends BaseMailboxController await _handleRefreshChangeMailboxSuccess(refreshState); } else { _clearNewFolderId(); - onDataFailureViewState(refreshState); + if (refreshState != null) { + onDataFailureViewState(refreshState); + } } } catch (e, stackTrace) { logWarning('MailboxController::_processMailboxStateQueue:Error processing state: $e'); diff --git a/lib/features/thread/presentation/thread_controller.dart b/lib/features/thread/presentation/thread_controller.dart index fd857d207..7e3b51384 100644 --- a/lib/features/thread/presentation/thread_controller.dart +++ b/lib/features/thread/presentation/thread_controller.dart @@ -100,6 +100,7 @@ class ThreadController extends BaseController with EmailActionController { bool canLoadMore = false; bool canSearchMore = false; MailboxId? _currentMemoryMailboxId; + int _peakEmailCount = 0; final ScrollController listEmailController = ScrollController(); final latestEmailSelectedOrUnselected = Rxn(); @visibleForTesting @@ -408,6 +409,20 @@ class ThreadController extends BaseController with EmailActionController { _checkIfCurrentMailboxCanLoadMore(); } }); + + ever(mailboxDashBoardController.emailsInCurrentMailbox, (emails) { + final countEmails = emails.length; + log( + 'ThreadController::Ever(mailboxDashBoardController.emailsInCurrentMailbox): ' + 'Count emails is $countEmails, ' + '_peakEmailCount is $_peakEmailCount', + ); + if (emails.isEmpty) { + _peakEmailCount = 0; + } else if (countEmails > _peakEmailCount) { + _peakEmailCount = countEmails; + } + }); } void _handleMarkEmailsAsReadByMailboxId(MailboxId mailboxId) { @@ -571,7 +586,7 @@ class ThreadController extends BaseController with EmailActionController { if (mailboxDashBoardController.isSelectionEnabled()) { mailboxDashBoardController.listEmailSelected.value = listEmailSelected; } - canLoadMore = newListEmail.length >= ThreadConstants.maxCountEmails; + canLoadMore = emailListSynced.length >= ThreadConstants.maxCountEmails; if (PlatformInfo.isWeb) { _validateBrowserHeight(); @@ -616,7 +631,7 @@ class ThreadController extends BaseController with EmailActionController { } canLoadMore = false; - loadingMoreStatus.value == LoadingMoreStatus.idle; + loadingMoreStatus.value = LoadingMoreStatus.idle; cancelSelectEmail(); if (searchController.isSearchEmailRunning) { @@ -627,11 +642,9 @@ class ThreadController extends BaseController with EmailActionController { } UnsignedInt get limitEmailFetched { - final emailsInCurrentMailbox = mailboxDashBoardController.emailsInCurrentMailbox; - final limit = emailsInCurrentMailbox.isNotEmpty - ? UnsignedInt(emailsInCurrentMailbox.length) - : ThreadConstants.defaultLimit; - return limit; + return _peakEmailCount == 0 + ? ThreadConstants.defaultLimit + : UnsignedInt(_peakEmailCount); } void _refreshEmailChanges({required jmap.State newState}) { @@ -699,6 +712,8 @@ class ThreadController extends BaseController with EmailActionController { needRefreshSearchState: true, ).last; + dispatchState(searchViewState); + final searchState = searchViewState .foldSuccessWithResult(); @@ -709,7 +724,9 @@ class ThreadController extends BaseController with EmailActionController { Left(RefreshAllEmailFailure())); canSearchMore = false; mailboxDashBoardController.emailsInCurrentMailbox.clear(); - onDataFailureViewState(searchState); + if (searchState != null) { + onDataFailureViewState(searchState); + } } } @@ -750,9 +767,11 @@ class ThreadController extends BaseController with EmailActionController { final refreshState = refreshViewState .foldSuccessWithResult(); + dispatchState(refreshViewState); + if (refreshState is RefreshChangesAllEmailSuccess) { _refreshChangesAllEmailSuccess(refreshState); - } else { + } else if (refreshState != null) { onDataFailureViewState(refreshState); } } @@ -781,12 +800,14 @@ class ThreadController extends BaseController with EmailActionController { ) .last; + dispatchState(viewState); + final emailSuccessState = viewState .foldSuccessWithResult(); if (emailSuccessState is GetAllEmailSuccess) { _getAllEmailSuccess(emailSuccessState); - } else { + } else if (emailSuccessState != null) { onDataFailureViewState(emailSuccessState); } } diff --git a/test/features/search/verify_before_time_in_search_email_filter_test.dart b/test/features/search/verify_before_time_in_search_email_filter_test.dart index f36c07c2e..e6cce030c 100644 --- a/test/features/search/verify_before_time_in_search_email_filter_test.dart +++ b/test/features/search/verify_before_time_in_search_email_filter_test.dart @@ -406,6 +406,7 @@ void main() { mockGetEmailByIdInteractor, mockCleanAndGetEmailsInMailboxInteractor, ); + threadController.onInit(); mailboxDashboardController.sessionCurrent = SessionFixtures.aliceSession; mailboxDashboardController.filterMessageOption.value = FilterMessageOption.all; diff --git a/test/features/thread/presentation/controller/thread_controller_test.dart b/test/features/thread/presentation/controller/thread_controller_test.dart index 5da43ea79..102634a00 100644 --- a/test/features/thread/presentation/controller/thread_controller_test.dart +++ b/test/features/thread/presentation/controller/thread_controller_test.dart @@ -266,6 +266,24 @@ void main() { }); group('_refreshEmailChanges::test', () { + late ThreadController refreshChangesController; + + setUp(() { + refreshChangesController = ThreadController( + mockGetEmailsInMailboxInteractor, + mockRefreshChangesEmailsInMailboxInteractor, + mockLoadMoreEmailsInMailboxInteractor, + mockSearchEmailInteractor, + mockSearchMoreEmailInteractor, + mockGetEmailByIdInteractor, + mockCleanAndGetEmailsInMailboxInteractor, + ); + }); + + tearDown(() { + refreshChangesController.onClose(); + }); + test( 'WHEN thread controller in searching\n' 'AND `MarkAsStarEmailSuccess` is coming\n' @@ -310,23 +328,24 @@ void main() { properties: anyNamed('properties'), needRefreshSearchState: anyNamed('needRefreshSearchState'), )).thenAnswer((_) => Stream.value(Right(SearchEmailSuccess(emailList)))); - + when(mockRefreshChangesEmailsInMailboxInteractor.execute( - any, - any, + any, + any, any, sort: anyNamed('sort'), limit: anyNamed('limit'), propertiesCreated: anyNamed('propertiesCreated'), propertiesUpdated: anyNamed('propertiesUpdated'), - emailFilter: anyNamed('emailFilter'), + emailFilter: anyNamed('emailFilter'), )).thenAnswer((_) => Stream.value(Right(RefreshChangesAllEmailSuccess( - emailList: emailList, + emailList: emailList, currentEmailState: State('old-state')))) ); // Act - threadController.onInit(); + refreshChangesController.onInit(); + mockMailboxDashBoardController.emailsInCurrentMailbox.refresh(); mockMailboxDashBoardController.emailUIAction.value = RefreshChangeEmailAction(newState: State('new-state')); @@ -357,7 +376,7 @@ void main() { )).called(1); expect(mockMailboxDashBoardController.emailsInCurrentMailbox.isNotEmpty, isTrue); expect(mockMailboxDashBoardController.emailsInCurrentMailbox.length, emailList.length); - expect(threadController.isListEmailScrollViewJumping, isFalse); + expect(refreshChangesController.isListEmailScrollViewJumping, isFalse); PlatformInfo.isTestingForWeb = false; }); @@ -406,7 +425,7 @@ void main() { )).thenAnswer((_) => Stream.value(Right(SearchEmailSuccess(emailList)))); // Act - threadController.onInit(); + refreshChangesController.onInit(); mockMailboxDashBoardController.dashBoardAction.value = StartSearchEmailAction(); await untilCalled(mockSearchEmailInteractor.execute( @@ -439,6 +458,24 @@ void main() { }); group('_registerObxStreamListener test:', () { + late ThreadController obxListenerController; + + setUp(() { + obxListenerController = ThreadController( + mockGetEmailsInMailboxInteractor, + mockRefreshChangesEmailsInMailboxInteractor, + mockLoadMoreEmailsInMailboxInteractor, + mockSearchEmailInteractor, + mockSearchMoreEmailInteractor, + mockGetEmailByIdInteractor, + mockCleanAndGetEmailsInMailboxInteractor, + ); + }); + + tearDown(() { + obxListenerController.onClose(); + }); + test( 'should call _getEmailsInMailboxInteractor.execute with getLatestChanges is false ' 'when mailboxDashBoardController.selectedMailbox updated', @@ -459,9 +496,9 @@ void main() { when(mockMailboxDashBoardController.currentSelectMode).thenReturn(Rx(SelectMode.INACTIVE)); when(mockMailboxDashBoardController.filterMessageOption).thenReturn(Rx(FilterMessageOption.all)); when(mockSearchController.searchState).thenReturn(SearchState(SearchStatus.INACTIVE).obs); - + // act - threadController.onInit(); + obxListenerController.onInit(); mockMailboxDashBoardController.selectedMailbox.value = mailboxAfter; await untilCalled(mockGetEmailsInMailboxInteractor.execute( any, @@ -487,5 +524,129 @@ void main() { )); }); }); + + group('limitEmailFetched::test', () { + late RxList emailsRxList; + late ThreadController limitEmailFetchedController; + + setUp(() { + emailsRxList = RxList(); + + limitEmailFetchedController = ThreadController( + mockGetEmailsInMailboxInteractor, + mockRefreshChangesEmailsInMailboxInteractor, + mockLoadMoreEmailsInMailboxInteractor, + mockSearchEmailInteractor, + mockSearchMoreEmailInteractor, + mockGetEmailByIdInteractor, + mockCleanAndGetEmailsInMailboxInteractor, + ); + + when(mockMailboxDashBoardController.selectedMailbox).thenReturn(Rxn(null)); + when(mockMailboxDashBoardController.searchController).thenReturn(mockSearchController); + when(mockMailboxDashBoardController.dashBoardAction).thenReturn(Rxn()); + when(mockMailboxDashBoardController.emailUIAction).thenReturn(Rxn()); + when(mockMailboxDashBoardController.viewState).thenReturn(Rx(Right(UIState.idle))); + when(mockMailboxDashBoardController.emailsInCurrentMailbox).thenReturn(emailsRxList); + when(mockMailboxDashBoardController.listEmailSelected).thenReturn(RxList()); + when(mockMailboxDashBoardController.currentSelectMode).thenReturn(Rx(SelectMode.INACTIVE)); + when(mockMailboxDashBoardController.filterMessageOption).thenReturn(Rx(FilterMessageOption.all)); + when(mockSearchController.searchState).thenReturn(SearchState(SearchStatus.INACTIVE).obs); + + limitEmailFetchedController.onInit(); + }); + + tearDown(() { + limitEmailFetchedController.onClose(); + }); + + List generateEmails(int count, {String prefix = 'email'}) { + return List.generate( + count, + (i) => PresentationEmail(id: EmailId(Id('$prefix$i'))), + ); + } + + test( + 'SHOULD return defaultLimit\n' + 'WHEN no emails loaded', + () { + // Assert + expect(limitEmailFetchedController.limitEmailFetched, ThreadConstants.defaultLimit); + }); + + test( + 'SHOULD return email count\n' + 'WHEN emails are loaded into the list', + () { + // Act + emailsRxList.addAll(generateEmails(40)); + + // Assert + expect(limitEmailFetchedController.limitEmailFetched, UnsignedInt(40)); + }); + + test( + 'SHOULD retain peak count\n' + 'WHEN emails are bulk deleted from the list', + () { + // Arrange + emailsRxList.addAll(generateEmails(40)); + + // Act - simulate bulk delete (removeWhere) + emailsRxList.removeRange(0, 20); + + // Assert - peak should still be 40, not 20 + expect(limitEmailFetchedController.limitEmailFetched, UnsignedInt(40)); + }); + + test( + 'SHOULD reset to defaultLimit\n' + 'WHEN resetToOriginalValue is called (mailbox switch)', + () { + // Arrange + emailsRxList.addAll(generateEmails(40)); + expect(limitEmailFetchedController.limitEmailFetched, UnsignedInt(40)); + + // Act + limitEmailFetchedController.resetToOriginalValue(); + + // Assert + expect(limitEmailFetchedController.limitEmailFetched, ThreadConstants.defaultLimit); + }); + + test( + 'SHOULD track peak across load-more then delete\n' + 'WHEN emails are loaded incrementally and then partially deleted', + () { + // Arrange - simulate initial load + 2 load-mores + emailsRxList.addAll(generateEmails(20, prefix: 'init')); + emailsRxList.addAll(generateEmails(20, prefix: 'more1')); + emailsRxList.addAll(generateEmails(20, prefix: 'more2')); + expect(limitEmailFetchedController.limitEmailFetched, UnsignedInt(60)); + + // Act - delete 30 emails + emailsRxList.removeRange(0, 30); + + // Assert - peak was 60 + expect(limitEmailFetchedController.limitEmailFetched, UnsignedInt(60)); + }); + + test( + 'SHOULD reset between mailbox switches\n' + 'WHEN switching to a mailbox with fewer emails', + () { + // Arrange - first mailbox has 40 emails + emailsRxList.addAll(generateEmails(40)); + expect(limitEmailFetchedController.limitEmailFetched, UnsignedInt(40)); + + // Act - switch mailbox (reset) then load fewer emails + limitEmailFetchedController.resetToOriginalValue(); + emailsRxList.addAll(generateEmails(15, prefix: 'new')); + + // Assert - peak should be 15, not stale 40 + expect(limitEmailFetchedController.limitEmailFetched, UnsignedInt(15)); + }); + }); }); } \ No newline at end of file