TF-3697 Open specific thread detail email

This commit is contained in:
DatDang
2025-05-23 16:00:18 +07:00
committed by Dat H. Pham
parent 9878b062d3
commit c9ca3925cb
13 changed files with 634 additions and 122 deletions
@@ -2,7 +2,14 @@ import 'package:core/presentation/state/failure.dart';
import 'package:core/presentation/state/success.dart'; import 'package:core/presentation/state/success.dart';
import 'package:model/email/presentation_email.dart'; import 'package:model/email/presentation_email.dart';
class GettingEmailsByIds extends LoadingState {} class GettingEmailsByIds extends LoadingState {
GettingEmailsByIds({this.loadingIndex});
final int? loadingIndex;
@override
List<Object?> get props => [loadingIndex];
}
class GetEmailsByIdsSuccess extends UIState { class GetEmailsByIdsSuccess extends UIState {
GetEmailsByIdsSuccess(this.presentationEmails); GetEmailsByIdsSuccess(this.presentationEmails);
@@ -22,6 +22,7 @@ class GetEmailsByIdsInteractor {
AccountId accountId, AccountId accountId,
List<EmailId> emailIds, { List<EmailId> emailIds, {
Properties? properties, Properties? properties,
int? loadMoreIndex,
}) async* { }) async* {
try { try {
if (emailIds.length > 1 && ( if (emailIds.length > 1 && (
@@ -31,7 +32,7 @@ class GetEmailsByIdsInteractor {
throw ThreadDetailOverloadException(); throw ThreadDetailOverloadException();
} }
yield Right(GettingEmailsByIds()); yield Right(GettingEmailsByIds(loadingIndex: loadMoreIndex));
final result = await _threadDetailRepository.getEmailsByIds( final result = await _threadDetailRepository.getEmailsByIds(
session, session,
accountId, accountId,
@@ -1,4 +1,3 @@
import 'package:collection/collection.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:model/email/email_action_type.dart'; import 'package:model/email/email_action_type.dart';
import 'package:model/extensions/presentation_email_extension.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/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_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/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/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/thread_detail_controller.dart';
import 'package:tmail_ui_user/features/thread_detail/presentation/widgets/thread_detail_collapsed_email.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 { extension GetThreadDetailEmailViews on ThreadDetailController {
List<Widget> getThreadDetailEmailViews() { List<Widget> getThreadDetailEmailViews() {
int? firstEmailNotLoadedIndex; final loadMoreSegments = Map<LoadMoreIndex, LoadMoreCount>.from(this.loadMoreSegments);
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);
}
}
return emailIdsPresentation.entries.map((entry) { return emailIdsPresentation.entries.map((entry) {
final emailId = entry.key; final emailId = entry.key;
final presentationEmail = entry.value; final presentationEmail = entry.value;
final indexOfEmailId = emailIdsPresentation.keys.toList().indexOf(emailId); final indexOfEmailId = emailIdsPresentation.keys.toList().indexOf(emailId);
if (presentationEmail == null) { if (presentationEmail == null) {
if (indexOfEmailId != firstEmailNotLoadedIndex) { if (loadMoreSegments[indexOfEmailId] == null) {
return const SizedBox.shrink(); return const SizedBox.shrink();
} }
return ThreadDetailLoadMoreCircle( return ThreadDetailLoadMoreCircle(
count: emailsToLoadMoreCount, count: loadMoreSegments[indexOfEmailId]!,
onTap: loadMoreThreadDetailEmails, onTap: () => loadMoreThreadDetailEmails(
loadMoreIndex: indexOfEmailId,
loadMoreCount: loadMoreSegments[indexOfEmailId]!,
),
imagePaths: imagePaths, imagePaths: imagePaths,
isLoading: loadingThreadDetail, isLoading: viewState.value.fold(
(failure) => false,
(success) => success is GettingEmailsByIds &&
success.loadingIndex == indexOfEmailId,
),
); );
} }
@@ -13,6 +13,7 @@ extension HandleGetEmailsByIdsSuccess on ThreadDetailController {
return; return;
} }
final selectedEmailId = mailboxDashBoardController.selectedEmail.value?.id;
final isLoadMore = emailIdsPresentation.values.whereNotNull().isNotEmpty; final isLoadMore = emailIdsPresentation.values.whereNotNull().isNotEmpty;
final currentScrollPosition = scrollController?.position.pixels; final currentScrollPosition = scrollController?.position.pixels;
final maxScrollExtent = scrollController?.position.maxScrollExtent; final maxScrollExtent = scrollController?.position.maxScrollExtent;
@@ -23,12 +24,12 @@ extension HandleGetEmailsByIdsSuccess on ThreadDetailController {
for (var presentationEmail in success.presentationEmails) { for (var presentationEmail in success.presentationEmails) {
if (presentationEmail.id == null) continue; if (presentationEmail.id == null) continue;
if (presentationEmail.id == emailIdsPresentation.keys.last) { if (presentationEmail.id == selectedEmailId) {
EmailBindings(currentEmailId: presentationEmail.id).dependencies(); EmailBindings(currentEmailId: presentationEmail.id).dependencies();
currentExpandedEmailId.value = presentationEmail.id; currentExpandedEmailId.value = presentationEmail.id;
} }
emailIdsPresentation[presentationEmail.id!] = presentationEmail.copyWith( emailIdsPresentation[presentationEmail.id!] = presentationEmail.copyWith(
emailInThreadStatus: presentationEmail.id == emailIdsPresentation.keys.last emailInThreadStatus: presentationEmail.id == selectedEmailId
? EmailInThreadStatus.expanded ? EmailInThreadStatus.expanded
: EmailInThreadStatus.collapsed, : EmailInThreadStatus.collapsed,
); );
@@ -7,9 +7,9 @@ import 'package:tmail_ui_user/features/thread_detail/presentation/utils/thread_d
extension InitializeThreadDetailEmails on ThreadDetailController { extension InitializeThreadDetailEmails on ThreadDetailController {
void initializeThreadDetailEmails() { void initializeThreadDetailEmails() {
final emailIdToLoadContent = emailIdsPresentation.keys.last; final emailIdsToLoadMetaData = ThreadDetailPresentationUtils.getFirstLoadEmailIds(
final emailIdsToLoadMetaData = ThreadDetailPresentationUtils.getEmailIdsToLoad( emailIdsPresentation.keys.toList(),
Map.from(emailIdsPresentation)..remove(emailIdToLoadContent), selectedEmailId: mailboxDashBoardController.selectedEmail.value?.id,
); );
if (accountId == null || session == null) { if (accountId == null || session == null) {
@@ -21,7 +21,7 @@ extension InitializeThreadDetailEmails on ThreadDetailController {
consumeState(getEmailsByIdsInteractor.execute( consumeState(getEmailsByIdsInteractor.execute(
session!, session!,
accountId!, accountId!,
[...emailIdsToLoadMetaData, emailIdToLoadContent], emailIdsToLoadMetaData,
properties: EmailUtils.getPropertiesForEmailGetMethod( properties: EmailUtils.getPropertiesForEmailGetMethod(
session!, session!,
accountId!, accountId!,
@@ -1,3 +1,5 @@
import 'dart:math';
import 'package:dartz/dartz.dart'; import 'package:dartz/dartz.dart';
import 'package:tmail_ui_user/features/email/presentation/utils/email_utils.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'; 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'; import 'package:tmail_ui_user/features/thread_detail/presentation/utils/thread_detail_presentation_utils.dart';
extension LoadMoreThreadDetailEmails on ThreadDetailController { extension LoadMoreThreadDetailEmails on ThreadDetailController {
void loadMoreThreadDetailEmails() { void loadMoreThreadDetailEmails({
required int loadMoreIndex,
required int loadMoreCount,
}) {
if (accountId == null || session == null) { if (accountId == null || session == null) {
consumeState(Stream.value(Left(GetEmailsByIdsFailure( consumeState(Stream.value(Left(GetEmailsByIdsFailure(
exception: NotFoundSessionException(), exception: NotFoundSessionException(),
@@ -14,8 +19,21 @@ extension LoadMoreThreadDetailEmails on ThreadDetailController {
return; return;
} }
final emailIdsToLoadMetaData = ThreadDetailPresentationUtils final currentExpandedEmailIndex = currentExpandedEmailId.value == null
.getEmailIdsToLoad(emailIdsPresentation); ? -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) { if (emailIdsToLoadMetaData.isEmpty) {
return; return;
} }
@@ -27,6 +45,7 @@ extension LoadMoreThreadDetailEmails on ThreadDetailController {
session!, session!,
accountId!, accountId!,
).union(additionalProperties), ).union(additionalProperties),
loadMoreIndex: loadMoreIndex,
)); ));
} }
} }
@@ -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<LoadMoreIndex, LoadMoreCount>;
extension ThreadDetailLoadMoreSegments on ThreadDetailController {
LoadMoreSegments get loadMoreSegments {
final loadMoreInfo = <LoadMoreIndex, LoadMoreCount>{};
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;
}
}
@@ -90,15 +90,6 @@ class ThreadDetailController extends BaseController {
PresentationMailbox.roleSent, PresentationMailbox.roleSent,
); );
String? get ownEmailAddress => session?.getOwnEmailAddress(); 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 { bool get isSearchRunning {
final isWebSearchRunning = mailboxDashBoardController final isWebSearchRunning = mailboxDashBoardController
.searchController .searchController
@@ -2,7 +2,6 @@ import 'dart:math';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:jmap_dart_client/jmap/mail/email/email.dart'; import 'package:jmap_dart_client/jmap/mail/email/email.dart';
import 'package:model/email/presentation_email.dart';
class ThreadDetailPresentationUtils { class ThreadDetailPresentationUtils {
const ThreadDetailPresentationUtils._(); const ThreadDetailPresentationUtils._();
@@ -11,29 +10,48 @@ class ThreadDetailPresentationUtils {
static const _defaultLoadSize = 20; static const _defaultLoadSize = 20;
@visibleForTesting @visibleForTesting
static get initialLoadSize => _initialLoadSize; static int get defaultLoadSize => _defaultLoadSize;
@visibleForTesting
static get defaultLoadSize => _defaultLoadSize;
static List<EmailId> getEmailIdsToLoad( static List<EmailId> getFirstLoadEmailIds(
Map<EmailId, PresentationEmail?> emailIdsPresentation, List<EmailId> emailIds, {
) { EmailId? selectedEmailId,
final validEmailIdsToLoad = emailIdsPresentation.entries }) {
.where((entry) => entry.value == null) // Default first, second and last email
.map((entry) => entry.key) if (emailIds.length <= 3) return emailIds;
.toList();
if (validEmailIdsToLoad.length == emailIdsPresentation.length) { if (selectedEmailId == null ||
// No email loaded yet !emailIds.contains(selectedEmailId) ||
return validEmailIdsToLoad.sublist( selectedEmailId == emailIds.first ||
0, selectedEmailId == emailIds.last ||
min(validEmailIdsToLoad.length, _initialLoadSize), selectedEmailId == emailIds.elementAt(1)) {
);
return [
...emailIds.sublist(
0,
min(emailIds.length, _initialLoadSize),
),
emailIds.last,
];
} }
return validEmailIdsToLoad.sublist( return [
validEmailIdsToLoad.length - min(validEmailIdsToLoad.length, _defaultLoadSize), emailIds.first,
validEmailIdsToLoad.length, selectedEmailId,
emailIds.last,
];
}
static getLoadMoreEmailIds(
List<EmailId> 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,
); );
} }
} }
@@ -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:jmap_dart_client/jmap/mail/email/email.dart';
import 'package:mockito/annotations.dart'; import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.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/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/extension/initialize_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/thread_detail_controller.dart';
@@ -16,15 +17,18 @@ import 'initialize_thread_detail_emails_test.mocks.dart';
@GenerateNiceMocks([ @GenerateNiceMocks([
MockSpec<GetEmailsByIdsInteractor>(), MockSpec<GetEmailsByIdsInteractor>(),
MockSpec<ThreadDetailController>(), MockSpec<ThreadDetailController>(),
MockSpec<MailboxDashBoardController>(),
]) ])
void main() { void main() {
group('InitializeThreadDetailEmails', () { group('InitializeThreadDetailEmails', () {
late MockGetEmailsByIdsInteractor getEmailsByIdsInteractor; late MockGetEmailsByIdsInteractor getEmailsByIdsInteractor;
late MockThreadDetailController threadDetailController; late MockThreadDetailController threadDetailController;
late MockMailboxDashBoardController mailboxDashboardController;
setUp(() { setUp(() {
getEmailsByIdsInteractor = MockGetEmailsByIdsInteractor(); getEmailsByIdsInteractor = MockGetEmailsByIdsInteractor();
threadDetailController = MockThreadDetailController(); threadDetailController = MockThreadDetailController();
mailboxDashboardController = MockMailboxDashBoardController();
}); });
test('initializeThreadDetailEmails should call getEmailsByIdsInteractor with 3 elements', () async { test('initializeThreadDetailEmails should call getEmailsByIdsInteractor with 3 elements', () async {
@@ -52,6 +56,8 @@ void main() {
final session = SessionFixtures.aliceSession; final session = SessionFixtures.aliceSession;
when(threadDetailController.accountId).thenReturn(accountId); when(threadDetailController.accountId).thenReturn(accountId);
when(threadDetailController.session).thenReturn(session); when(threadDetailController.session).thenReturn(session);
when(threadDetailController.mailboxDashBoardController).thenReturn(mailboxDashboardController);
when(mailboxDashboardController.selectedEmail).thenReturn(Rxn());
// Act // Act
threadDetailController.initializeThreadDetailEmails(); threadDetailController.initializeThreadDetailEmails();
@@ -7,9 +7,11 @@ import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart'; import 'package:mockito/mockito.dart';
import 'package:model/email/presentation_email.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/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/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/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/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/account_fixtures.dart';
import '../../../../fixtures/session_fixtures.dart'; import '../../../../fixtures/session_fixtures.dart';
@@ -18,55 +20,66 @@ import 'load_more_thread_detail_emails_test.mocks.dart';
@GenerateNiceMocks([ @GenerateNiceMocks([
MockSpec<ThreadDetailController>(), MockSpec<ThreadDetailController>(),
MockSpec<GetEmailsByIdsInteractor>(), MockSpec<GetEmailsByIdsInteractor>(),
MockSpec<MailboxDashBoardController>(),
]) ])
void main() { void main() {
late MockThreadDetailController controller; late MockThreadDetailController controller;
late MockGetEmailsByIdsInteractor getEmailsByIdsInteractor; late MockGetEmailsByIdsInteractor getEmailsByIdsInteractor;
late MockMailboxDashBoardController mailboxDashBoardController;
setUp(() { setUp(() {
controller = MockThreadDetailController(); controller = MockThreadDetailController();
getEmailsByIdsInteractor = MockGetEmailsByIdsInteractor(); getEmailsByIdsInteractor = MockGetEmailsByIdsInteractor();
mailboxDashBoardController = MockMailboxDashBoardController();
}); });
test('Only call getEmailsByIdsInteractor.execute on emails where presentation email is null, max 20 emails', () async { test('Only call getEmailsByIdsInteractor.execute on emails where presentation email is null, max 20 emails', () async {
// Arrange // Arrange
when(controller.emailIdsPresentation).thenReturn({ when(controller.emailIdsPresentation).thenReturn({
for (int i = 0; i < 40; i++) for (int i = 0; i < 40; i++) EmailId(Id('$i')): null,
EmailId(Id('$i')): null,
EmailId(Id('40')): PresentationEmail(),
}.obs); }.obs);
when(controller.currentExpandedEmailId).thenReturn(Rxn());
when(controller.session).thenReturn(SessionFixtures.aliceSession); when(controller.session).thenReturn(SessionFixtures.aliceSession);
when(controller.accountId).thenReturn(AccountFixtures.aliceAccountId); when(controller.accountId).thenReturn(AccountFixtures.aliceAccountId);
when(controller.getEmailsByIdsInteractor).thenReturn(getEmailsByIdsInteractor); when(controller.getEmailsByIdsInteractor).thenReturn(getEmailsByIdsInteractor);
when(controller.additionalProperties).thenReturn(Properties.empty()); when(controller.additionalProperties).thenReturn(Properties.empty());
when(controller.mailboxDashBoardController).thenReturn(mailboxDashBoardController);
when(mailboxDashBoardController.selectedEmail).thenReturn(Rxn());
// Act // Act
controller.loadMoreThreadDetailEmails(); controller.loadMoreThreadDetailEmails(
loadMoreIndex: 0,
loadMoreCount: controller.emailIdsPresentation.length,
);
// Assert // Assert
verify(getEmailsByIdsInteractor.execute( verify(getEmailsByIdsInteractor.execute(
SessionFixtures.aliceSession, SessionFixtures.aliceSession,
AccountFixtures.aliceAccountId, AccountFixtures.aliceAccountId,
List.generate(20, (i) => EmailId(Id('${i + 20}'))), List.generate(20, (i) => EmailId(Id('$i'))),
properties: EmailUtils.getPropertiesForEmailGetMethod( properties: EmailUtils.getPropertiesForEmailGetMethod(
SessionFixtures.aliceSession, SessionFixtures.aliceSession,
AccountFixtures.aliceAccountId, AccountFixtures.aliceAccountId,
), ),
loadMoreIndex: 0,
)).called(1); )).called(1);
}); });
test('No getEmailsByIdsInteractor.execute call if emailIdsToLoadMetaData is empty', () async { test('No getEmailsByIdsInteractor.execute call if emailIdsToLoadMetaData is empty', () async {
// Arrange // Arrange
when(controller.emailIdsPresentation).thenReturn({ when(controller.emailIdsPresentation).thenReturn(<EmailId, PresentationEmail?>{}.obs);
EmailId(Id('1')): PresentationEmail(), when(controller.currentExpandedEmailId).thenReturn(Rxn());
EmailId(Id('2')): PresentationEmail(),
}.obs);
when(controller.session).thenReturn(SessionFixtures.aliceSession); when(controller.session).thenReturn(SessionFixtures.aliceSession);
when(controller.accountId).thenReturn(AccountFixtures.aliceAccountId); when(controller.accountId).thenReturn(AccountFixtures.aliceAccountId);
when(controller.getEmailsByIdsInteractor).thenReturn(getEmailsByIdsInteractor); when(controller.getEmailsByIdsInteractor).thenReturn(getEmailsByIdsInteractor);
when(controller.mailboxDashBoardController).thenReturn(mailboxDashBoardController);
when(mailboxDashBoardController.selectedEmail).thenReturn(Rxn());
// Act // Act
controller.loadMoreThreadDetailEmails(); controller.loadMoreThreadDetailEmails(
loadMoreIndex: 0,
loadMoreCount: controller.emailIdsPresentation.length,
);
// Assert // Assert
verifyNever(getEmailsByIdsInteractor.execute( verifyNever(getEmailsByIdsInteractor.execute(
@@ -81,17 +94,21 @@ void main() {
// Arrange // Arrange
const limit = 15; const limit = 15;
when(controller.emailIdsPresentation).thenReturn({ when(controller.emailIdsPresentation).thenReturn({
for (int i = 0; i < limit; i++) for (int i = 0; i < limit; i++) EmailId(Id('$i')): null,
EmailId(Id('$i')): null,
EmailId(Id('$limit')): PresentationEmail(),
}.obs); }.obs);
when(controller.currentExpandedEmailId).thenReturn(Rxn());
when(controller.session).thenReturn(SessionFixtures.aliceSession); when(controller.session).thenReturn(SessionFixtures.aliceSession);
when(controller.accountId).thenReturn(AccountFixtures.aliceAccountId); when(controller.accountId).thenReturn(AccountFixtures.aliceAccountId);
when(controller.getEmailsByIdsInteractor).thenReturn(getEmailsByIdsInteractor); when(controller.getEmailsByIdsInteractor).thenReturn(getEmailsByIdsInteractor);
when(controller.additionalProperties).thenReturn(Properties.empty()); when(controller.additionalProperties).thenReturn(Properties.empty());
when(controller.mailboxDashBoardController).thenReturn(mailboxDashBoardController);
when(mailboxDashBoardController.selectedEmail).thenReturn(Rxn());
// Act // Act
controller.loadMoreThreadDetailEmails(); controller.loadMoreThreadDetailEmails(
loadMoreIndex: 0,
loadMoreCount: controller.emailIdsPresentation.length,
);
// Assert // Assert
verify(getEmailsByIdsInteractor.execute( verify(getEmailsByIdsInteractor.execute(
@@ -102,6 +119,96 @@ void main() {
SessionFixtures.aliceSession, SessionFixtures.aliceSession,
AccountFixtures.aliceAccountId, 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); )).called(1);
}); });
} }
@@ -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<ThreadDetailController>()])
void main() {
late MockThreadDetailController controller;
setUp(() {
controller = MockThreadDetailController();
});
group('ThreadDetailLoadMoreSegments', () {
test(
'should return empty map when emailIdsPresentation is empty',
() {
// arrange
when(controller.emailIdsPresentation).thenReturn(<EmailId, PresentationEmail?>{}.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, {});
});
});
}
@@ -1,68 +1,326 @@
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:jmap_dart_client/jmap/core/id.dart'; import 'package:jmap_dart_client/jmap/core/id.dart';
import 'package:jmap_dart_client/jmap/mail/email/email.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'; import 'package:tmail_ui_user/features/thread_detail/presentation/utils/thread_detail_presentation_utils.dart';
void main() { void main() {
group('ThreadDetailPresentationUtils', () { group('ThreadDetailPresentationUtils', () {
group('getEmailIdsToLoad', () { group('getEmailIdsToLoad', () {
test('loads first time with length equal or more than initial load size', () { test(
final emailIdsPresentation = { 'should return empty list '
EmailId(Id('1')): null, 'when emailIds is empty',
EmailId(Id('2')): null, () {
EmailId(Id('3')): null, // arrange
EmailId(Id('4')): null, final emailIds = <EmailId>[];
EmailId(Id('5')): null,
}; // act
final result = ThreadDetailPresentationUtils.getFirstLoadEmailIds(
final result = ThreadDetailPresentationUtils.getEmailIdsToLoad(emailIdsPresentation); emailIds,
);
expect(result.length, equals(ThreadDetailPresentationUtils.initialLoadSize));
expect(result, equals([EmailId(Id('1')), EmailId(Id('2'))])); // assert
expect(result, isEmpty);
}); });
test('loads first time with length smaller than initial load size', () { test(
final emailIdsPresentation = { 'should return all email ids in emailIds '
EmailId(Id('1')): null, 'when isFirstLoad is true '
EmailId(Id('2')): null, 'and emailIds contains equal to or less than 3 email ids',
}; () {
// arrange
final result = ThreadDetailPresentationUtils.getEmailIdsToLoad(emailIdsPresentation); final emailIds = [
EmailId(Id('1')),
expect(result.length, equals(emailIdsPresentation.length)); EmailId(Id('2')),
expect(result, equals([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', () { test(
final emailIdsPresentation = { 'should return first two emails and last email '
EmailId(Id('1')): PresentationEmail(), 'when isFirstLoad is true '
EmailId(Id('2')): null, 'and emailIds contains more than 3 email ids '
EmailId(Id('3')): null, 'and selectedEmailId is not null ',
EmailId(Id('4')): null, () {
EmailId(Id('5')): null, // arrange
EmailId(Id('6')): null, final emailIds = [
EmailId(Id('7')): null, EmailId(Id('1')),
}; EmailId(Id('2')),
EmailId(Id('3')),
final result = ThreadDetailPresentationUtils.getEmailIdsToLoad(emailIdsPresentation); EmailId(Id('4')),
EmailId(Id('5')),
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'))]));
// 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', () { test(
final emailIdsPresentation = { 'should return first two emails and last email '
EmailId(Id('1')): PresentationEmail(), 'when isFirstLoad is true '
EmailId(Id('2')): null, 'and emailIds contains more than 3 email ids '
EmailId(Id('3')): null, '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)); test(
expect(result, equals([EmailId(Id('2')), EmailId(Id('3'))])); '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,
)),
);
}); });
}); });
}); });
} }