diff --git a/lib/features/thread_detail/domain/state/get_emails_by_ids_state.dart b/lib/features/thread_detail/domain/state/get_emails_by_ids_state.dart index 468e52662..4a30d1152 100644 --- a/lib/features/thread_detail/domain/state/get_emails_by_ids_state.dart +++ b/lib/features/thread_detail/domain/state/get_emails_by_ids_state.dart @@ -2,7 +2,14 @@ import 'package:core/presentation/state/failure.dart'; import 'package:core/presentation/state/success.dart'; import 'package:model/email/presentation_email.dart'; -class GettingEmailsByIds extends LoadingState {} +class GettingEmailsByIds extends LoadingState { + GettingEmailsByIds({this.loadingIndex}); + + final int? loadingIndex; + + @override + List get props => [loadingIndex]; +} class GetEmailsByIdsSuccess extends UIState { GetEmailsByIdsSuccess(this.presentationEmails); diff --git a/lib/features/thread_detail/domain/usecases/get_emails_by_ids_interactor.dart b/lib/features/thread_detail/domain/usecases/get_emails_by_ids_interactor.dart index 2f905de44..8a0200832 100644 --- a/lib/features/thread_detail/domain/usecases/get_emails_by_ids_interactor.dart +++ b/lib/features/thread_detail/domain/usecases/get_emails_by_ids_interactor.dart @@ -22,6 +22,7 @@ class GetEmailsByIdsInteractor { AccountId accountId, List emailIds, { Properties? properties, + int? loadMoreIndex, }) async* { try { if (emailIds.length > 1 && ( @@ -31,7 +32,7 @@ class GetEmailsByIdsInteractor { throw ThreadDetailOverloadException(); } - yield Right(GettingEmailsByIds()); + yield Right(GettingEmailsByIds(loadingIndex: loadMoreIndex)); final result = await _threadDetailRepository.getEmailsByIds( session, accountId, 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 2ccccd715..a97a55864 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 @@ -1,4 +1,3 @@ -import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:model/email/email_action_type.dart'; import 'package:model/extensions/presentation_email_extension.dart'; @@ -8,6 +7,8 @@ import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/extensions import 'package:tmail_ui_user/features/thread_detail/presentation/extension/load_more_thread_detail_emails.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/extension/thread_detail_open_email_address_detail_action.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/presentation/extension/thread_detail_load_more_segments.dart'; import 'package:tmail_ui_user/features/thread_detail/presentation/extension/toggle_thread_detail_collape_expand.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_collapsed_email.dart'; @@ -16,35 +17,29 @@ import 'package:tmail_ui_user/main/routes/route_navigation.dart'; extension GetThreadDetailEmailViews on ThreadDetailController { List getThreadDetailEmailViews() { - int? firstEmailNotLoadedIndex; - if (emailsToLoadMoreCount > 0) { - final firstNotLoadedEmailId = emailIdsPresentation.entries.firstWhereOrNull( - (entry) => entry.value == null - )?.key; - if (firstNotLoadedEmailId == null) { - firstEmailNotLoadedIndex = -1; - } else { - firstEmailNotLoadedIndex = emailIdsPresentation - .keys - .toList() - .indexOf(firstNotLoadedEmailId); - } - } + final loadMoreSegments = Map.from(this.loadMoreSegments); return emailIdsPresentation.entries.map((entry) { final emailId = entry.key; final presentationEmail = entry.value; final indexOfEmailId = emailIdsPresentation.keys.toList().indexOf(emailId); if (presentationEmail == null) { - if (indexOfEmailId != firstEmailNotLoadedIndex) { + if (loadMoreSegments[indexOfEmailId] == null) { return const SizedBox.shrink(); } return ThreadDetailLoadMoreCircle( - count: emailsToLoadMoreCount, - onTap: loadMoreThreadDetailEmails, + count: loadMoreSegments[indexOfEmailId]!, + onTap: () => loadMoreThreadDetailEmails( + loadMoreIndex: indexOfEmailId, + loadMoreCount: loadMoreSegments[indexOfEmailId]!, + ), imagePaths: imagePaths, - isLoading: loadingThreadDetail, + isLoading: viewState.value.fold( + (failure) => false, + (success) => success is GettingEmailsByIds && + success.loadingIndex == indexOfEmailId, + ), ); } 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 ab8f42625..78c938310 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 @@ -13,6 +13,7 @@ extension HandleGetEmailsByIdsSuccess on ThreadDetailController { return; } + final selectedEmailId = mailboxDashBoardController.selectedEmail.value?.id; final isLoadMore = emailIdsPresentation.values.whereNotNull().isNotEmpty; final currentScrollPosition = scrollController?.position.pixels; final maxScrollExtent = scrollController?.position.maxScrollExtent; @@ -23,12 +24,12 @@ extension HandleGetEmailsByIdsSuccess on ThreadDetailController { for (var presentationEmail in success.presentationEmails) { if (presentationEmail.id == null) continue; - if (presentationEmail.id == emailIdsPresentation.keys.last) { + if (presentationEmail.id == selectedEmailId) { EmailBindings(currentEmailId: presentationEmail.id).dependencies(); currentExpandedEmailId.value = presentationEmail.id; } emailIdsPresentation[presentationEmail.id!] = presentationEmail.copyWith( - emailInThreadStatus: presentationEmail.id == emailIdsPresentation.keys.last + emailInThreadStatus: presentationEmail.id == selectedEmailId ? EmailInThreadStatus.expanded : EmailInThreadStatus.collapsed, ); 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 a1fbbee10..6590d81fa 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 @@ -7,9 +7,9 @@ import 'package:tmail_ui_user/features/thread_detail/presentation/utils/thread_d extension InitializeThreadDetailEmails on ThreadDetailController { void initializeThreadDetailEmails() { - final emailIdToLoadContent = emailIdsPresentation.keys.last; - final emailIdsToLoadMetaData = ThreadDetailPresentationUtils.getEmailIdsToLoad( - Map.from(emailIdsPresentation)..remove(emailIdToLoadContent), + final emailIdsToLoadMetaData = ThreadDetailPresentationUtils.getFirstLoadEmailIds( + emailIdsPresentation.keys.toList(), + selectedEmailId: mailboxDashBoardController.selectedEmail.value?.id, ); if (accountId == null || session == null) { @@ -21,7 +21,7 @@ extension InitializeThreadDetailEmails on ThreadDetailController { consumeState(getEmailsByIdsInteractor.execute( session!, accountId!, - [...emailIdsToLoadMetaData, emailIdToLoadContent], + emailIdsToLoadMetaData, properties: EmailUtils.getPropertiesForEmailGetMethod( session!, accountId!, diff --git a/lib/features/thread_detail/presentation/extension/load_more_thread_detail_emails.dart b/lib/features/thread_detail/presentation/extension/load_more_thread_detail_emails.dart index c3961c6bb..914790cfa 100644 --- a/lib/features/thread_detail/presentation/extension/load_more_thread_detail_emails.dart +++ b/lib/features/thread_detail/presentation/extension/load_more_thread_detail_emails.dart @@ -1,3 +1,5 @@ +import 'dart:math'; + import 'package:dartz/dartz.dart'; import 'package:tmail_ui_user/features/email/presentation/utils/email_utils.dart'; import 'package:tmail_ui_user/features/home/data/exceptions/session_exceptions.dart'; @@ -6,7 +8,10 @@ import 'package:tmail_ui_user/features/thread_detail/presentation/thread_detail_ import 'package:tmail_ui_user/features/thread_detail/presentation/utils/thread_detail_presentation_utils.dart'; extension LoadMoreThreadDetailEmails on ThreadDetailController { - void loadMoreThreadDetailEmails() { + void loadMoreThreadDetailEmails({ + required int loadMoreIndex, + required int loadMoreCount, + }) { if (accountId == null || session == null) { consumeState(Stream.value(Left(GetEmailsByIdsFailure( exception: NotFoundSessionException(), @@ -14,8 +19,21 @@ extension LoadMoreThreadDetailEmails on ThreadDetailController { return; } - final emailIdsToLoadMetaData = ThreadDetailPresentationUtils - .getEmailIdsToLoad(emailIdsPresentation); + final currentExpandedEmailIndex = currentExpandedEmailId.value == null + ? -1 + : emailIdsPresentation.keys.toList().indexOf(currentExpandedEmailId.value!); + final loadMoreEmailIds = emailIdsPresentation.keys.toList().sublist( + loadMoreIndex, + min( + loadMoreIndex + loadMoreCount, + emailIdsPresentation.length, + ), + ); + + final emailIdsToLoadMetaData = ThreadDetailPresentationUtils.getLoadMoreEmailIds( + loadMoreEmailIds, + loadEmailsAfterSelectedEmail: loadMoreIndex > currentExpandedEmailIndex, + ); if (emailIdsToLoadMetaData.isEmpty) { return; } @@ -27,6 +45,7 @@ extension LoadMoreThreadDetailEmails on ThreadDetailController { session!, accountId!, ).union(additionalProperties), + loadMoreIndex: loadMoreIndex, )); } } \ No newline at end of file diff --git a/lib/features/thread_detail/presentation/extension/thread_detail_load_more_segments.dart b/lib/features/thread_detail/presentation/extension/thread_detail_load_more_segments.dart new file mode 100644 index 000000000..0e36d0799 --- /dev/null +++ b/lib/features/thread_detail/presentation/extension/thread_detail_load_more_segments.dart @@ -0,0 +1,33 @@ +import 'package:tmail_ui_user/features/thread_detail/presentation/thread_detail_controller.dart'; + +typedef LoadMoreIndex = int; +typedef LoadMoreCount = int; +typedef LoadMoreSegments = Map; + +extension ThreadDetailLoadMoreSegments on ThreadDetailController { + LoadMoreSegments get loadMoreSegments { + final loadMoreInfo = {}; + int currentIndex = 0; + final emailIds = emailIdsPresentation.values.toList(); + + while (currentIndex < emailIdsPresentation.length) { + if (emailIds[currentIndex] == null) { + // Found start of a null segment + int segmentIndex = currentIndex; + int segmentCount = 0; + + // Count consecutive nulls + while (currentIndex < emailIdsPresentation.length && + emailIds[currentIndex] == null) { + segmentCount++; + currentIndex++; + } + + loadMoreInfo[segmentIndex] = segmentCount; + } else { + currentIndex++; + } + } + return loadMoreInfo; + } +} \ No newline at end of file diff --git a/lib/features/thread_detail/presentation/thread_detail_controller.dart b/lib/features/thread_detail/presentation/thread_detail_controller.dart index 0c89ea05d..131f32268 100644 --- a/lib/features/thread_detail/presentation/thread_detail_controller.dart +++ b/lib/features/thread_detail/presentation/thread_detail_controller.dart @@ -90,15 +90,6 @@ class ThreadDetailController extends BaseController { PresentationMailbox.roleSent, ); String? get ownEmailAddress => session?.getOwnEmailAddress(); - int get emailsToLoadMoreCount => emailIdsPresentation - .values - .where((email) => email == null) - .length; - bool get loadingThreadDetail => viewState.value.fold( - (failure) => false, - (success) => success is GettingThreadById - || success is GettingEmailsByIds, - ); bool get isSearchRunning { final isWebSearchRunning = mailboxDashBoardController .searchController diff --git a/lib/features/thread_detail/presentation/utils/thread_detail_presentation_utils.dart b/lib/features/thread_detail/presentation/utils/thread_detail_presentation_utils.dart index 32f0165f4..8adf35599 100644 --- a/lib/features/thread_detail/presentation/utils/thread_detail_presentation_utils.dart +++ b/lib/features/thread_detail/presentation/utils/thread_detail_presentation_utils.dart @@ -2,7 +2,6 @@ import 'dart:math'; import 'package:flutter/material.dart'; import 'package:jmap_dart_client/jmap/mail/email/email.dart'; -import 'package:model/email/presentation_email.dart'; class ThreadDetailPresentationUtils { const ThreadDetailPresentationUtils._(); @@ -11,29 +10,48 @@ class ThreadDetailPresentationUtils { static const _defaultLoadSize = 20; @visibleForTesting - static get initialLoadSize => _initialLoadSize; - @visibleForTesting - static get defaultLoadSize => _defaultLoadSize; + static int get defaultLoadSize => _defaultLoadSize; - static List getEmailIdsToLoad( - Map emailIdsPresentation, - ) { - final validEmailIdsToLoad = emailIdsPresentation.entries - .where((entry) => entry.value == null) - .map((entry) => entry.key) - .toList(); + static List getFirstLoadEmailIds( + List emailIds, { + EmailId? selectedEmailId, + }) { + // Default first, second and last email + if (emailIds.length <= 3) return emailIds; - if (validEmailIdsToLoad.length == emailIdsPresentation.length) { - // No email loaded yet - return validEmailIdsToLoad.sublist( - 0, - min(validEmailIdsToLoad.length, _initialLoadSize), - ); + if (selectedEmailId == null || + !emailIds.contains(selectedEmailId) || + selectedEmailId == emailIds.first || + selectedEmailId == emailIds.last || + selectedEmailId == emailIds.elementAt(1)) { + + return [ + ...emailIds.sublist( + 0, + min(emailIds.length, _initialLoadSize), + ), + emailIds.last, + ]; } - return validEmailIdsToLoad.sublist( - validEmailIdsToLoad.length - min(validEmailIdsToLoad.length, _defaultLoadSize), - validEmailIdsToLoad.length, + return [ + emailIds.first, + selectedEmailId, + emailIds.last, + ]; + } + + static getLoadMoreEmailIds( + List emailIds, { + bool loadEmailsAfterSelectedEmail = false, + }) { + if (loadEmailsAfterSelectedEmail) { + return emailIds.sublist(0, min(emailIds.length, _defaultLoadSize)); + } + + return emailIds.sublist( + emailIds.length - min(_defaultLoadSize, emailIds.length), + emailIds.length, ); } } \ No newline at end of file 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 5f889ef08..0ca679894 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 @@ -6,6 +6,7 @@ import 'package:jmap_dart_client/jmap/core/properties/properties.dart'; import 'package:jmap_dart_client/jmap/mail/email/email.dart'; import 'package:mockito/annotations.dart'; import 'package:mockito/mockito.dart'; +import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.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/extension/initialize_thread_detail_emails.dart'; import 'package:tmail_ui_user/features/thread_detail/presentation/thread_detail_controller.dart'; @@ -16,15 +17,18 @@ import 'initialize_thread_detail_emails_test.mocks.dart'; @GenerateNiceMocks([ MockSpec(), MockSpec(), + MockSpec(), ]) void main() { group('InitializeThreadDetailEmails', () { late MockGetEmailsByIdsInteractor getEmailsByIdsInteractor; late MockThreadDetailController threadDetailController; + late MockMailboxDashBoardController mailboxDashboardController; setUp(() { getEmailsByIdsInteractor = MockGetEmailsByIdsInteractor(); threadDetailController = MockThreadDetailController(); + mailboxDashboardController = MockMailboxDashBoardController(); }); test('initializeThreadDetailEmails should call getEmailsByIdsInteractor with 3 elements', () async { @@ -52,6 +56,8 @@ void main() { final session = SessionFixtures.aliceSession; when(threadDetailController.accountId).thenReturn(accountId); when(threadDetailController.session).thenReturn(session); + when(threadDetailController.mailboxDashBoardController).thenReturn(mailboxDashboardController); + when(mailboxDashboardController.selectedEmail).thenReturn(Rxn()); // Act threadDetailController.initializeThreadDetailEmails(); diff --git a/test/features/thread_detail/presentation/extension/load_more_thread_detail_emails_test.dart b/test/features/thread_detail/presentation/extension/load_more_thread_detail_emails_test.dart index 7177ecd07..ebdfb568c 100644 --- a/test/features/thread_detail/presentation/extension/load_more_thread_detail_emails_test.dart +++ b/test/features/thread_detail/presentation/extension/load_more_thread_detail_emails_test.dart @@ -7,9 +7,11 @@ import 'package:mockito/annotations.dart'; import 'package:mockito/mockito.dart'; import 'package:model/email/presentation_email.dart'; import 'package:tmail_ui_user/features/email/presentation/utils/email_utils.dart'; +import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.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/extension/load_more_thread_detail_emails.dart'; import 'package:tmail_ui_user/features/thread_detail/presentation/thread_detail_controller.dart'; +import 'package:tmail_ui_user/features/thread_detail/presentation/utils/thread_detail_presentation_utils.dart'; import '../../../../fixtures/account_fixtures.dart'; import '../../../../fixtures/session_fixtures.dart'; @@ -18,55 +20,66 @@ import 'load_more_thread_detail_emails_test.mocks.dart'; @GenerateNiceMocks([ MockSpec(), MockSpec(), + MockSpec(), ]) void main() { late MockThreadDetailController controller; late MockGetEmailsByIdsInteractor getEmailsByIdsInteractor; + late MockMailboxDashBoardController mailboxDashBoardController; setUp(() { controller = MockThreadDetailController(); getEmailsByIdsInteractor = MockGetEmailsByIdsInteractor(); + mailboxDashBoardController = MockMailboxDashBoardController(); }); test('Only call getEmailsByIdsInteractor.execute on emails where presentation email is null, max 20 emails', () async { // Arrange when(controller.emailIdsPresentation).thenReturn({ - for (int i = 0; i < 40; i++) - EmailId(Id('$i')): null, - EmailId(Id('40')): PresentationEmail(), + for (int i = 0; i < 40; i++) EmailId(Id('$i')): null, }.obs); + when(controller.currentExpandedEmailId).thenReturn(Rxn()); when(controller.session).thenReturn(SessionFixtures.aliceSession); when(controller.accountId).thenReturn(AccountFixtures.aliceAccountId); when(controller.getEmailsByIdsInteractor).thenReturn(getEmailsByIdsInteractor); when(controller.additionalProperties).thenReturn(Properties.empty()); + when(controller.mailboxDashBoardController).thenReturn(mailboxDashBoardController); + when(mailboxDashBoardController.selectedEmail).thenReturn(Rxn()); // Act - controller.loadMoreThreadDetailEmails(); + controller.loadMoreThreadDetailEmails( + loadMoreIndex: 0, + loadMoreCount: controller.emailIdsPresentation.length, + ); // Assert verify(getEmailsByIdsInteractor.execute( SessionFixtures.aliceSession, AccountFixtures.aliceAccountId, - List.generate(20, (i) => EmailId(Id('${i + 20}'))), + List.generate(20, (i) => EmailId(Id('$i'))), properties: EmailUtils.getPropertiesForEmailGetMethod( SessionFixtures.aliceSession, AccountFixtures.aliceAccountId, ), + loadMoreIndex: 0, )).called(1); }); test('No getEmailsByIdsInteractor.execute call if emailIdsToLoadMetaData is empty', () async { // Arrange - when(controller.emailIdsPresentation).thenReturn({ - EmailId(Id('1')): PresentationEmail(), - EmailId(Id('2')): PresentationEmail(), - }.obs); + when(controller.emailIdsPresentation).thenReturn({}.obs); + when(controller.currentExpandedEmailId).thenReturn(Rxn()); when(controller.session).thenReturn(SessionFixtures.aliceSession); when(controller.accountId).thenReturn(AccountFixtures.aliceAccountId); when(controller.getEmailsByIdsInteractor).thenReturn(getEmailsByIdsInteractor); + when(controller.mailboxDashBoardController).thenReturn(mailboxDashBoardController); + when(mailboxDashBoardController.selectedEmail).thenReturn(Rxn()); // Act - controller.loadMoreThreadDetailEmails(); + controller.loadMoreThreadDetailEmails( + loadMoreIndex: 0, + loadMoreCount: controller.emailIdsPresentation.length, + ); // Assert verifyNever(getEmailsByIdsInteractor.execute( @@ -81,17 +94,21 @@ void main() { // Arrange const limit = 15; when(controller.emailIdsPresentation).thenReturn({ - for (int i = 0; i < limit; i++) - EmailId(Id('$i')): null, - EmailId(Id('$limit')): PresentationEmail(), + for (int i = 0; i < limit; i++) EmailId(Id('$i')): null, }.obs); + when(controller.currentExpandedEmailId).thenReturn(Rxn()); when(controller.session).thenReturn(SessionFixtures.aliceSession); when(controller.accountId).thenReturn(AccountFixtures.aliceAccountId); when(controller.getEmailsByIdsInteractor).thenReturn(getEmailsByIdsInteractor); when(controller.additionalProperties).thenReturn(Properties.empty()); + when(controller.mailboxDashBoardController).thenReturn(mailboxDashBoardController); + when(mailboxDashBoardController.selectedEmail).thenReturn(Rxn()); // Act - controller.loadMoreThreadDetailEmails(); + controller.loadMoreThreadDetailEmails( + loadMoreIndex: 0, + loadMoreCount: controller.emailIdsPresentation.length, + ); // Assert verify(getEmailsByIdsInteractor.execute( @@ -102,6 +119,96 @@ void main() { SessionFixtures.aliceSession, AccountFixtures.aliceAccountId, ), + loadMoreIndex: 0, + )).called(1); + }); + + + test( + 'should call getEmailsByIdsInteractor.execute ' + 'with ThreadDetailPresentationUtils.defaultLoadSize (20) top to bottom ' + 'when loadMoreIndex is larger than index of currentExpandedEmailId', + () { + // arrange + final loadMoreEmails = { + for (int i = 31; i < 60; i++) EmailId(Id('$i')): null + }; + const loadMoreIndex = 32; + when(controller.emailIdsPresentation).thenReturn({ + for (int i = 0; i < 30; i++) EmailId(Id('$i')): null, + EmailId(Id('30')): PresentationEmail(), + ...loadMoreEmails, + }.obs); + when(controller.currentExpandedEmailId).thenReturn(Rxn(EmailId(Id('30')))); + when(controller.session).thenReturn(SessionFixtures.aliceSession); + when(controller.accountId).thenReturn(AccountFixtures.aliceAccountId); + when(controller.getEmailsByIdsInteractor).thenReturn(getEmailsByIdsInteractor); + when(controller.mailboxDashBoardController).thenReturn(mailboxDashBoardController); + when(mailboxDashBoardController.selectedEmail).thenReturn(Rxn()); + + // act + controller.loadMoreThreadDetailEmails( + loadMoreIndex: loadMoreIndex, + loadMoreCount: loadMoreEmails.length, + ); + + // assert + verify(getEmailsByIdsInteractor.execute( + SessionFixtures.aliceSession, + AccountFixtures.aliceAccountId, + List.generate( + ThreadDetailPresentationUtils.defaultLoadSize, + (i) => EmailId(Id('${i + loadMoreIndex}')), + ), + properties: EmailUtils.getPropertiesForEmailGetMethod( + SessionFixtures.aliceSession, + AccountFixtures.aliceAccountId, + ), + loadMoreIndex: loadMoreIndex, + )).called(1); + }); + + test( + 'should call getEmailsByIdsInteractor.execute ' + 'with ThreadDetailPresentationUtils.defaultLoadSize (20) bottom to top ' + 'when loadMoreIndex is larger than index of currentExpandedEmailId', + () { + // arrange + final loadMoreEmails = { + for (int i = 0; i < 30; i++) EmailId(Id('$i')): null + }; + const loadMoreIndex = 0; + when(controller.emailIdsPresentation).thenReturn({ + ...loadMoreEmails, + EmailId(Id('30')): PresentationEmail(), + for (int i = 31; i < 60; i++) EmailId(Id('$i')): null, + }.obs); + when(controller.currentExpandedEmailId).thenReturn(Rxn(EmailId(Id('30')))); + when(controller.session).thenReturn(SessionFixtures.aliceSession); + when(controller.accountId).thenReturn(AccountFixtures.aliceAccountId); + when(controller.getEmailsByIdsInteractor).thenReturn(getEmailsByIdsInteractor); + when(controller.mailboxDashBoardController).thenReturn(mailboxDashBoardController); + when(mailboxDashBoardController.selectedEmail).thenReturn(Rxn()); + + // act + controller.loadMoreThreadDetailEmails( + loadMoreIndex: loadMoreIndex, + loadMoreCount: loadMoreEmails.length, + ); + + // assert + verify(getEmailsByIdsInteractor.execute( + SessionFixtures.aliceSession, + AccountFixtures.aliceAccountId, + List.generate( + ThreadDetailPresentationUtils.defaultLoadSize, + (i) => EmailId(Id('${loadMoreEmails.length - i - 1}')), + ).reversed.toList(), + properties: EmailUtils.getPropertiesForEmailGetMethod( + SessionFixtures.aliceSession, + AccountFixtures.aliceAccountId, + ), + loadMoreIndex: loadMoreIndex, )).called(1); }); } diff --git a/test/features/thread_detail/presentation/extension/thread_detail_load_more_segments_test.dart b/test/features/thread_detail/presentation/extension/thread_detail_load_more_segments_test.dart new file mode 100644 index 000000000..d7ce3d4cb --- /dev/null +++ b/test/features/thread_detail/presentation/extension/thread_detail_load_more_segments_test.dart @@ -0,0 +1,76 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:get/get.dart'; +import 'package:jmap_dart_client/jmap/core/id.dart'; +import 'package:jmap_dart_client/jmap/mail/email/email.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; +import 'package:model/email/presentation_email.dart'; +import 'package:tmail_ui_user/features/thread_detail/presentation/extension/thread_detail_load_more_segments.dart'; +import 'package:tmail_ui_user/features/thread_detail/presentation/thread_detail_controller.dart'; + +import 'thread_detail_load_more_segments_test.mocks.dart'; + +@GenerateNiceMocks([MockSpec()]) +void main() { + late MockThreadDetailController controller; + + setUp(() { + controller = MockThreadDetailController(); + }); + + group('ThreadDetailLoadMoreSegments', () { + test( + 'should return empty map when emailIdsPresentation is empty', + () { + // arrange + when(controller.emailIdsPresentation).thenReturn({}.obs); + + // act + final result = controller.loadMoreSegments; + + // assert + expect(result, {}); + }); + + test( + 'should return correct load more segments when there are null values in emailIdsPresentation', + () { + // arrange + when(controller.emailIdsPresentation).thenReturn({ + EmailId(Id('1')): PresentationEmail(), + EmailId(Id('2')): null, + EmailId(Id('3')): null, + EmailId(Id('4')): PresentationEmail(), + EmailId(Id('5')): null, + }.obs); + + // act + final result = controller.loadMoreSegments; + + // assert + expect(result, { + 1: 2, + 4: 1, + }); + }); + + test( + 'should return correct load more segments when there are no null values in emailIdsPresentation', + () { + // arrange + when(controller.emailIdsPresentation).thenReturn({ + EmailId(Id('1')): PresentationEmail(), + EmailId(Id('2')): PresentationEmail(), + EmailId(Id('3')): PresentationEmail(), + EmailId(Id('4')): PresentationEmail(), + EmailId(Id('5')): PresentationEmail(), + }.obs); + + // act + final result = controller.loadMoreSegments; + + // assert + expect(result, {}); + }); + }); +} \ No newline at end of file diff --git a/test/features/thread_detail/presentation/utils/thread_detail_presentation_utils_test.dart b/test/features/thread_detail/presentation/utils/thread_detail_presentation_utils_test.dart index 13212f3cc..7b9382807 100644 --- a/test/features/thread_detail/presentation/utils/thread_detail_presentation_utils_test.dart +++ b/test/features/thread_detail/presentation/utils/thread_detail_presentation_utils_test.dart @@ -1,68 +1,326 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:jmap_dart_client/jmap/core/id.dart'; import 'package:jmap_dart_client/jmap/mail/email/email.dart'; -import 'package:model/email/presentation_email.dart'; import 'package:tmail_ui_user/features/thread_detail/presentation/utils/thread_detail_presentation_utils.dart'; void main() { group('ThreadDetailPresentationUtils', () { group('getEmailIdsToLoad', () { - test('loads first time with length equal or more than initial load size', () { - final emailIdsPresentation = { - EmailId(Id('1')): null, - EmailId(Id('2')): null, - EmailId(Id('3')): null, - EmailId(Id('4')): null, - EmailId(Id('5')): null, - }; - - final result = ThreadDetailPresentationUtils.getEmailIdsToLoad(emailIdsPresentation); - - expect(result.length, equals(ThreadDetailPresentationUtils.initialLoadSize)); - expect(result, equals([EmailId(Id('1')), EmailId(Id('2'))])); + test( + 'should return empty list ' + 'when emailIds is empty', + () { + // arrange + final emailIds = []; + + // act + final result = ThreadDetailPresentationUtils.getFirstLoadEmailIds( + emailIds, + ); + + // assert + expect(result, isEmpty); }); - test('loads first time with length smaller than initial load size', () { - final emailIdsPresentation = { - EmailId(Id('1')): null, - EmailId(Id('2')): null, - }; - - final result = ThreadDetailPresentationUtils.getEmailIdsToLoad(emailIdsPresentation); - - expect(result.length, equals(emailIdsPresentation.length)); - expect(result, equals([EmailId(Id('1')), EmailId(Id('2'))])); + test( + 'should return all email ids in emailIds ' + 'when isFirstLoad is true ' + 'and emailIds contains equal to or less than 3 email ids', + () { + // arrange + final emailIds = [ + EmailId(Id('1')), + EmailId(Id('2')), + EmailId(Id('3')), + ]; + + // act + final result = ThreadDetailPresentationUtils.getFirstLoadEmailIds( + emailIds, + ); + + // assert + expect(result, equals(emailIds)); }); - test('loads with length equal or more than default load size', () { - final emailIdsPresentation = { - EmailId(Id('1')): PresentationEmail(), - EmailId(Id('2')): null, - EmailId(Id('3')): null, - EmailId(Id('4')): null, - EmailId(Id('5')): null, - EmailId(Id('6')): null, - EmailId(Id('7')): null, - }; - - final result = ThreadDetailPresentationUtils.getEmailIdsToLoad(emailIdsPresentation); - - expect(result.length, equals(6)); - expect(result, equals([EmailId(Id('2')), EmailId(Id('3')), EmailId(Id('4')), EmailId(Id('5')), EmailId(Id('6')), EmailId(Id('7'))])); + test( + 'should return first two emails and last email ' + 'when isFirstLoad is true ' + 'and emailIds contains more than 3 email ids ' + 'and selectedEmailId is not null ', + () { + // arrange + final emailIds = [ + EmailId(Id('1')), + EmailId(Id('2')), + EmailId(Id('3')), + EmailId(Id('4')), + EmailId(Id('5')), + ]; + + // act + final result = ThreadDetailPresentationUtils.getFirstLoadEmailIds( + emailIds, + ); + + // assert + expect( + result, + equals([EmailId(Id('1')), EmailId(Id('2')), EmailId(Id('5'))]), + ); }); - test('loads with length smaller than default load size', () { - final emailIdsPresentation = { - EmailId(Id('1')): PresentationEmail(), - EmailId(Id('2')): null, - EmailId(Id('3')): null, - }; + test( + 'should return first two emails and last email ' + 'when isFirstLoad is true ' + 'and emailIds contains more than 3 email ids ' + 'and selectedEmailId is not inside emailIds', + () { + // arrange + final emailIds = [ + EmailId(Id('1')), + EmailId(Id('2')), + EmailId(Id('3')), + EmailId(Id('4')), + EmailId(Id('5')), + ]; + + // act + final result = ThreadDetailPresentationUtils.getFirstLoadEmailIds( + emailIds, + selectedEmailId: EmailId(Id('6')), + ); + + // assert + expect( + result, + equals([EmailId(Id('1')), EmailId(Id('2')), EmailId(Id('5'))]), + ); + }); - final result = ThreadDetailPresentationUtils.getEmailIdsToLoad(emailIdsPresentation); + test( + 'should return first two emails and last email ' + 'when isFirstLoad is true ' + 'and emailIds contains more than 3 email ids ' + 'and selectedEmailId is first element of emailIds', + () { + // arrange + final emailIds = [ + EmailId(Id('1')), + EmailId(Id('2')), + EmailId(Id('3')), + EmailId(Id('4')), + EmailId(Id('5')), + ]; + + // act + final result = ThreadDetailPresentationUtils.getFirstLoadEmailIds( + emailIds, + selectedEmailId: emailIds.first, + ); + + // assert + expect( + result, + equals([EmailId(Id('1')), EmailId(Id('2')), EmailId(Id('5'))]), + ); + }); - expect(result.length, equals(emailIdsPresentation.length - 1)); - expect(result, equals([EmailId(Id('2')), EmailId(Id('3'))])); + test( + 'should return first two emails and last email ' + 'when isFirstLoad is true ' + 'and emailIds contains more than 3 email ids ' + 'and selectedEmailId is last element of emailIds', + () { + // arrange + final emailIds = [ + EmailId(Id('1')), + EmailId(Id('2')), + EmailId(Id('3')), + EmailId(Id('4')), + EmailId(Id('5')), + ]; + + // act + final result = ThreadDetailPresentationUtils.getFirstLoadEmailIds( + emailIds, + selectedEmailId: emailIds.last, + ); + + // assert + expect( + result, + equals([EmailId(Id('1')), EmailId(Id('2')), EmailId(Id('5'))]), + ); + }); + + test( + 'should return first two emails and last email ' + 'when isFirstLoad is true ' + 'and emailIds contains more than 3 email ids ' + 'and selectedEmailId is second element of emailIds', + () { + // arrange + final emailIds = [ + EmailId(Id('1')), + EmailId(Id('2')), + EmailId(Id('3')), + EmailId(Id('4')), + EmailId(Id('5')), + ]; + + // act + final result = ThreadDetailPresentationUtils.getFirstLoadEmailIds( + emailIds, + selectedEmailId: emailIds.elementAt(1), + ); + + // assert + expect( + result, + equals([EmailId(Id('1')), EmailId(Id('2')), EmailId(Id('5'))]), + ); + }); + + test( + 'should return first, last and selected email ' + 'when emailIds contains more than 3 email ids ' + 'and isFirstLoad is true ' + 'and selectedEmailId is inside emailIds ' + 'and selectedEmailId is not first, last or second element of emailIds', + () { + // arrange + final emailIds = [ + EmailId(Id('1')), + EmailId(Id('2')), + EmailId(Id('3')), + EmailId(Id('4')), + EmailId(Id('5')), + ]; + final selectedEmail = EmailId(Id('3')); + + // act + final result = ThreadDetailPresentationUtils.getFirstLoadEmailIds( + emailIds, + selectedEmailId: selectedEmail, + ); + + // assert + expect( + result, + equals([emailIds.first, selectedEmail, emailIds.last]), + ); + }); + + test( + 'should return all emails ' + 'when emailIds contains more than 3 email ids ' + 'and isFirstLoad is false ' + 'and loadEmailsAfterSelectedEmail is true ' + 'and emailsIds.length is less than defaultLoadSize', + () { + // arrange + final emailIds = [ + EmailId(Id('1')), + EmailId(Id('2')), + EmailId(Id('3')), + EmailId(Id('4')), + EmailId(Id('5')), + ]; + + // act + final result = ThreadDetailPresentationUtils.getLoadMoreEmailIds( + emailIds, + loadEmailsAfterSelectedEmail: true, + ); + + // assert + expect( + result, + equals(emailIds), + ); + }); + + test( + 'should return first defaultLoadSize (20) emails ' + 'when emailIds contains more than 3 email ids ' + 'and isFirstLoad is false ' + 'and loadEmailsAfterSelectedEmail is true ' + 'and emailsIds.length is more than defaultLoadSize', + () { + // arrange + final emailIds = List.generate(30, (i) => EmailId(Id('$i'))); + + // act + final result = ThreadDetailPresentationUtils.getLoadMoreEmailIds( + emailIds, + loadEmailsAfterSelectedEmail: true, + ); + + // assert + expect( + result, + equals(emailIds.sublist( + 0, + ThreadDetailPresentationUtils.defaultLoadSize, + )), + ); + }); + + test( + 'should return all emails ' + 'when emailIds contains more than 3 email ids ' + 'and isFirstLoad is false ' + 'and loadEmailsAfterSelectedEmail is false ' + 'and emailsIds.length is less than defaultLoadSize', + () { + // arrange + final emailIds = [ + EmailId(Id('1')), + EmailId(Id('2')), + EmailId(Id('3')), + EmailId(Id('4')), + EmailId(Id('5')), + ]; + + // act + final result = ThreadDetailPresentationUtils.getLoadMoreEmailIds( + emailIds, + loadEmailsAfterSelectedEmail: false, + ); + + // assert + expect( + result, + equals(emailIds), + ); + }); + + test( + 'should return last defaultLoadSize (20) emails ' + 'when emailIds contains more than 3 email ids ' + 'and isFirstLoad is false ' + 'and loadEmailsAfterSelectedEmail is false ' + 'and emailsIds.length is more than defaultLoadSize', + () { + // arrange + final emailIds = List.generate(30, (i) => EmailId(Id('$i'))); + + // act + final result = ThreadDetailPresentationUtils.getLoadMoreEmailIds( + emailIds, + loadEmailsAfterSelectedEmail: false, + ); + + // assert + expect( + result, + equals(emailIds.sublist( + emailIds.length - ThreadDetailPresentationUtils.defaultLoadSize, + emailIds.length, + )), + ); }); }); }); -} \ No newline at end of file +}