TF-3643 Thread Detail get emails meta data method

TF-3643 Thread Detail cache email with thread id

TF-3643 Thread Detail view thread detail

TF-3643 Thread Detail [Part 2 fixup] remove ThreadDetailController dependency from MailboxDashboardController

TF-3643 Thread Detail [Part 2 fixup] remove EmailSupervisorController dependency from ThreadDetailController

TF-3643 Thread Detail [Part 2 fixup] move isSearchRunning check into ThreadDetailController

TF-3643 Thread Detail [Part 2 fixup] ensure GetEmailsByIdsInteractor performance

TF-3643 Thread Detail [Part 2 fixup] unit test loadMoreThreadDetailEmails

TF-3643 Thread Detail [Part 2 fixup] unit test getEmailIdsToLoad

TF-3643 Thread Detail [Part 2 fixup] optimize ThreadDetailBinding bindingsDataSource

TF-3643 Thread Detail [Part 2 fixup] optimize getThreadDetailLoadingView

TF-3643 Thread Detail [Part 2 fixup] readd unique ValueKey for HtmlContentViewerOnWebWidget

TF-3643 Thread Detail [Part 2 fixup] unit test initializeThreadDetailEmails

TF-3643 Thread Detail [Part 3 fixup] Update private action button methods

TF-3643 Thread Detail [Part 4 fixup] Collapsed email preview 65 characters max

TF-3643 Thread Detail [Part 4 fixup] Remove personal scripts

TF-3643 Thread Detail move EmailInThreadStatus into PresentationEmail

TF-3643 Thread Detail refactor toggle collapse expand

TF-3643 Thread Detail EmailViewAppBarWidget required parameters

TF-3643 Thread Detail tap avatar toggle expansion

TF-3643 Thread Detail fix cannot open single email

TF-3643 Thread Detail fix open thread first time blank view

TF-3643 Thread Detail fix thread detail UI

TF-3643 Thread Detail fix round bottom view

TF-3643 Thread Detail fix round bottom view

TF-3643 Thread Detail bottom actions depend on current expanded email

TF-3643 Thread Detail hide next previous actions

TF-3643 Thread Detail fix load more show initial loading view

TF-3643 Thread Detail fix cannot view single email

TF-3643 Thread Detail fix bottom place holder not aligned

TF-3643 Thread Detail fix back to search view

TF-3643 Thread Detail fix display 65 character preview collapsed email

TF-3643 Thread Detail fix search not work

TF-3643 Thread Detail fix bottom place holder UI

TF-3643 Thread Detail fix expand view

TF-3643 Thread Detail jump to top of last email view

TF-3643 Thread Detail remove toMe condition of filtering thread

TF-3643 Thread Detail fix sort thread detail

TF-3643 Thread Detail fix assert not work in release

TF-3643 Thread Detail add load more test case <20

TF-3643 Thread Detail remove the use of currentThreadId

TF-3643 Thread Detail remove the use of selectedEmail in SingleEmailController

TF-3643 Thread Detail fix unit test SingleEmailController calendar

TF-3643 Thread Detail disable 2 way scroll control

TF-3643 Thread Detail allow thread to load sent email incase thread only contains 1 email

TF-3643 Thread Detail disable single email page view

TF-3643 Thread Detail update emailsNotLoadedCount name

TF-3643 Thread Detail remove collapsed email char limit

TF-3643 Thread Detail fix CI

TF-3643 Thread Detail unit test handleGetEmailIdsByThreadIdSuccess

TF-3643 Thread Detail make _currentEmailId private

TF-3643 Thread Detail fix search click users

TF-3643 Thread Detail fix scroll jump

TF-3643 Thread Detail fix scroll jump

TF-3643 Thread Detail fix web back action

TF-3643 Thread Detail round bottom actions

TF-3643 Thread Detail fix tablet large empty email

TF-3643 Thread Detail fix email subject selectable

TF-3643 Thread Detail fix email single view with top divider

TF-3643 Thread Detail fix email cache threadId missing

TF-3643 Thread Detail fix filter sent email in thread details

TF-3643 Thread Detail fix select text

TF-3643 Thread Detail fix subject alignment

TF-3643 Thread Detail fix subject alignment

TF-3643 Thread Detail fix tablet large empty email

TF-3643 Thread Detail add nullcheck for _jumpScrollViewToTopOfEmail

TF-3643 Thread Detail decouple ThreadDetailController from SingleEmailController

TF-3643 Thread Detail combine emailIds to emailIdsPresentation

TF-3643 Thread Detail remove currentEmailLoaded obx listener of SingleEmailController

TF-3643 Thread Detail replace threadAction with an email ui action

TF-3643 Thread Detail fix top and bottom bar delay displaying

TF-3643 Thread Detail update ReplyForwardBottomBarAction to PerformEmailActionInThreadDetailAction

TF-3643 Thread Detail use getOwnEmailAddress in ThreadDetailAppBar

TF-3643 Thread Detail add TextOverflow.ellipsis to EmailSubjectWidget

TF-3643 Thread Detail fix round border threadDetailLoadingView
This commit is contained in:
DatDang
2025-05-12 15:22:29 +07:00
committed by Dat H. Pham
parent 7c3af832e4
commit 58a576adeb
84 changed files with 2845 additions and 558 deletions
@@ -239,14 +239,14 @@ void main() {
when(mailboxDashboardController.emailUIAction).thenReturn(Rxn(null));
when(mailboxDashboardController.viewState).thenReturn(Rx(Right(UIState.idle)));
when(mailboxDashboardController.sessionCurrent).thenReturn(testSession);
singleEmailController.onInit();
Get.put<AcceptCalendarEventInteractor>(acceptCalendarEventInteractor);
mailboxDashboardController.accountId.refresh();
singleEmailController.handleSuccessViewState(
ParseCalendarEventSuccess([
BlobCalendarEvent(
blobId: blobId,
calendarEventList: [calendarEvent])]));
singleEmailController.onInit();
mailboxDashboardController.accountId.refresh();
singleEmailController.handleSuccessViewState(
ParseCalendarEventSuccess([
BlobCalendarEvent(
blobId: blobId,
calendarEventList: [calendarEvent])]));
// act
singleEmailController.onCalendarEventReplyAction(EventActionType.yes, emailId);
@@ -267,8 +267,8 @@ void main() {
when(mailboxDashboardController.emailUIAction).thenReturn(Rxn(null));
when(mailboxDashboardController.viewState).thenReturn(Rx(Right(UIState.idle)));
when(mailboxDashboardController.sessionCurrent).thenReturn(testSession);
singleEmailController.onInit();
Get.put<MaybeCalendarEventInteractor>(maybeCalendarEventInteractor);
singleEmailController.onInit();
mailboxDashboardController.accountId.refresh();
singleEmailController.handleSuccessViewState(
ParseCalendarEventSuccess([
@@ -295,8 +295,8 @@ void main() {
when(mailboxDashboardController.emailUIAction).thenReturn(Rxn(null));
when(mailboxDashboardController.viewState).thenReturn(Rx(Right(UIState.idle)));
when(mailboxDashboardController.sessionCurrent).thenReturn(testSession);
singleEmailController.onInit();
Get.put<RejectCalendarEventInteractor>(rejectCalendarEventInteractor);
singleEmailController.onInit();
mailboxDashboardController.accountId.refresh();
singleEmailController.handleSuccessViewState(
ParseCalendarEventSuccess([
@@ -0,0 +1,52 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:jmap_dart_client/jmap/core/id.dart';
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:model/email/email_property.dart';
import 'package:tmail_ui_user/features/thread_detail/domain/exceptions/thread_detail_overload_exception.dart';
import 'package:tmail_ui_user/features/thread_detail/domain/repository/thread_detail_repository.dart';
import 'package:tmail_ui_user/features/thread_detail/domain/state/get_emails_by_ids_state.dart';
import 'package:tmail_ui_user/features/thread_detail/domain/usecases/get_emails_by_ids_interactor.dart';
import '../../../../fixtures/account_fixtures.dart';
import '../../../../fixtures/session_fixtures.dart';
import 'get_emails_by_ids_interactor_test.mocks.dart';
@GenerateNiceMocks([MockSpec<ThreadDetailRepository>()])
void main() {
final repository = MockThreadDetailRepository();
final getEmailsByIdsInteractor = GetEmailsByIdsInteractor(repository);
group('get emails by ids interactor test:', () {
test(
'should throw ThreadDetailOverloadException '
'when there are more than 1 emailIds '
'and properties contains any of htmlBody, bodyValues or attachments',
() async {
// arrange
final emailIds = [EmailId(Id('1')), EmailId(Id('2'))];
final properties = Properties({
EmailProperty.htmlBody,
EmailProperty.bodyValues,
EmailProperty.attachments,
});
final result = await getEmailsByIdsInteractor.execute(
SessionFixtures.aliceSession,
AccountFixtures.aliceAccountId,
emailIds,
properties: properties,
).last;
// assert
expect(
result.fold(
(failure) => failure is GetEmailsByIdsFailure &&
failure.exception is ThreadDetailOverloadException,
(success) => false,
),
true,
);
});
});
}
@@ -0,0 +1,68 @@
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/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart';
import 'package:tmail_ui_user/features/thread_detail/domain/state/get_thread_by_id_state.dart';
import 'package:tmail_ui_user/features/thread_detail/presentation/extension/handle_get_email_ids_by_thread_id_success.dart';
import 'package:tmail_ui_user/features/thread_detail/presentation/thread_detail_controller.dart';
import 'handle_get_email_ids_by_thread_id_success_test.mocks.dart';
@GenerateNiceMocks([
MockSpec<ThreadDetailController>(),
MockSpec<MailboxDashBoardController>(),
])
void main() {
final threadDetailController = MockThreadDetailController();
group('handle get email ids by thread id success test:', () {
test(
'should assign emailIds with result from success '
'when result from success is not empty',
() {
// arrange
final success = GetThreadByIdSuccess([
EmailId(Id('1')),
EmailId(Id('2')),
]);
when(threadDetailController.emailIdsPresentation).thenReturn(
<EmailId, PresentationEmail?>{}.obs,
);
// act
threadDetailController.handleGetEmailIdsByThreadIdSuccess(success);
// assert
expect(threadDetailController.emailIdsPresentation.keys, success.emailIds);
});
test(
'should assign emailIds with result from mailbox dashboard controller '
'when result from success is empty '
'and mailbox dashboard controller selected email is not null',
() {
// arrange
final mailboxDashBoardController = MockMailboxDashBoardController();
final success = GetThreadByIdSuccess([]);
when(mailboxDashBoardController.selectedEmail).thenReturn(
Rxn(PresentationEmail(id: EmailId(Id('1')))),
);
when(threadDetailController.emailIdsPresentation).thenReturn(
<EmailId, PresentationEmail?>{}.obs,
);
when(threadDetailController.mailboxDashBoardController).thenReturn(mailboxDashBoardController);
// act
threadDetailController.handleGetEmailIdsByThreadIdSuccess(success);
// assert
expect(
threadDetailController.emailIdsPresentation.keys,
[mailboxDashBoardController.selectedEmail.value!.id!],
);
});
});
}
@@ -0,0 +1,68 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:get/get.dart';
import 'package:jmap_dart_client/jmap/account_id.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: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';
import '../../../../fixtures/session_fixtures.dart';
import 'initialize_thread_detail_emails_test.mocks.dart';
@GenerateNiceMocks([
MockSpec<GetEmailsByIdsInteractor>(),
MockSpec<ThreadDetailController>(),
])
void main() {
group('InitializeThreadDetailEmails', () {
late MockGetEmailsByIdsInteractor getEmailsByIdsInteractor;
late MockThreadDetailController threadDetailController;
setUp(() {
getEmailsByIdsInteractor = MockGetEmailsByIdsInteractor();
threadDetailController = MockThreadDetailController();
});
test('initializeThreadDetailEmails should call getEmailsByIdsInteractor with 3 elements', () async {
// Arrange
final emailIds = [
EmailId(Id('email1')),
EmailId(Id('email2')),
EmailId(Id('email3')),
EmailId(Id('email4')),
EmailId(Id('email5')),
];
when(threadDetailController.emailIdsPresentation).thenReturn(
Map.fromEntries(emailIds.map((emailId) => MapEntry(emailId, null))).obs,
);
when(threadDetailController.getEmailsByIdsInteractor).thenReturn(getEmailsByIdsInteractor);
when(getEmailsByIdsInteractor.execute(
any,
any,
any,
properties: anyNamed('properties'),
)).thenAnswer((_) => const Stream.empty());
final accountId = AccountId(Id('accountId'));
final session = SessionFixtures.aliceSession;
when(threadDetailController.accountId).thenReturn(accountId);
when(threadDetailController.session).thenReturn(session);
// Act
threadDetailController.initializeThreadDetailEmails();
// Assert
final captured = verify(getEmailsByIdsInteractor.execute(
captureAny,
captureAny,
captureAny,
properties: captureAnyNamed('properties'),
)).captured;
expect((captured[2] as List<EmailId>).length, 3);
});
});
}
@@ -0,0 +1,104 @@
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/email/presentation/utils/email_utils.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 '../../../../fixtures/account_fixtures.dart';
import '../../../../fixtures/session_fixtures.dart';
import 'load_more_thread_detail_emails_test.mocks.dart';
@GenerateNiceMocks([
MockSpec<ThreadDetailController>(),
MockSpec<GetEmailsByIdsInteractor>(),
])
void main() {
late MockThreadDetailController controller;
late MockGetEmailsByIdsInteractor getEmailsByIdsInteractor;
setUp(() {
controller = MockThreadDetailController();
getEmailsByIdsInteractor = MockGetEmailsByIdsInteractor();
});
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(),
}.obs);
when(controller.session).thenReturn(SessionFixtures.aliceSession);
when(controller.accountId).thenReturn(AccountFixtures.aliceAccountId);
when(controller.getEmailsByIdsInteractor).thenReturn(getEmailsByIdsInteractor);
// Act
controller.loadMoreThreadDetailEmails();
// Assert
verify(getEmailsByIdsInteractor.execute(
SessionFixtures.aliceSession,
AccountFixtures.aliceAccountId,
List.generate(20, (i) => EmailId(Id('${i + 20}'))),
properties: EmailUtils.getPropertiesForEmailGetMethod(
SessionFixtures.aliceSession,
AccountFixtures.aliceAccountId,
),
)).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.session).thenReturn(SessionFixtures.aliceSession);
when(controller.accountId).thenReturn(AccountFixtures.aliceAccountId);
when(controller.getEmailsByIdsInteractor).thenReturn(getEmailsByIdsInteractor);
// Act
controller.loadMoreThreadDetailEmails();
// Assert
verifyNever(getEmailsByIdsInteractor.execute(
any,
any,
any,
properties: anyNamed('properties'),
));
});
test('Call getEmailsByIdsInteractor.execute when there are less than 20 emails', () async {
// Arrange
const limit = 15;
when(controller.emailIdsPresentation).thenReturn({
for (int i = 0; i < limit; i++)
EmailId(Id('$i')): null,
EmailId(Id('$limit')): PresentationEmail(),
}.obs);
when(controller.session).thenReturn(SessionFixtures.aliceSession);
when(controller.accountId).thenReturn(AccountFixtures.aliceAccountId);
when(controller.getEmailsByIdsInteractor).thenReturn(getEmailsByIdsInteractor);
// Act
controller.loadMoreThreadDetailEmails();
// Assert
verify(getEmailsByIdsInteractor.execute(
SessionFixtures.aliceSession,
AccountFixtures.aliceAccountId,
List.generate(limit, (i) => EmailId(Id('$i'))),
properties: EmailUtils.getPropertiesForEmailGetMethod(
SessionFixtures.aliceSession,
AccountFixtures.aliceAccountId,
),
)).called(1);
});
}
@@ -0,0 +1,68 @@
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('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('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('loads with length smaller than default load size', () {
final emailIdsPresentation = {
EmailId(Id('1')): PresentationEmail(),
EmailId(Id('2')): null,
EmailId(Id('3')): null,
};
final result = ThreadDetailPresentationUtils.getEmailIdsToLoad(emailIdsPresentation);
expect(result.length, equals(emailIdsPresentation.length - 1));
expect(result, equals([EmailId(Id('2')), EmailId(Id('3'))]));
});
});
});
}