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
@@ -0,0 +1,21 @@
import 'package:flutter/material.dart';
import 'package:tmail_ui_user/features/email/presentation/action/email_ui_action.dart';
import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/model/dashboard_routes.dart';
import 'package:tmail_ui_user/features/thread_detail/presentation/thread_detail_controller.dart';
extension CloseThreadDetailAction on ThreadDetailController {
void closeThreadDetailAction(BuildContext? context) {
if (isSearchRunning) {
mailboxDashBoardController.dispatchRoute(DashboardRoutes.searchEmail);
} else {
mailboxDashBoardController.dispatchRoute(DashboardRoutes.thread);
}
for (var emailId in emailIdsPresentation.keys) {
mailboxDashBoardController.dispatchEmailUIAction(
CloseEmailInThreadDetailAction(emailId),
);
}
reset();
}
}
@@ -0,0 +1,14 @@
import 'package:flutter/material.dart';
import 'package:jmap_dart_client/jmap/mail/email/email.dart';
import 'package:tmail_ui_user/features/thread_detail/presentation/thread_detail_controller.dart';
extension FocusThreadDetailExpandedEmail on ThreadDetailController {
void focusExpandedEmail(EmailId emailId) {
Future.delayed(const Duration(milliseconds: 200), () {
final context = GlobalObjectKey(emailId.id.value).currentContext;
if (context != null && context.mounted) {
Scrollable.ensureVisible(context);
}
});
}
}
@@ -0,0 +1,35 @@
import 'package:core/presentation/views/loading/cupertino_loading_widget.dart';
import 'package:flutter/material.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/thread_detail_controller.dart';
extension GetThreadDetailLoadingView on ThreadDetailController {
Widget getThreadDetailLoadingView({
required bool isResponsiveDesktop,
}) {
final isLoading = viewState.value.fold(
(failure) => false,
(success) => success is GettingThreadById
);
if (!isLoading) return const SizedBox.shrink();
return Expanded(
child: Container(
alignment: Alignment.center,
decoration: const BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.all(Radius.circular(20)),
),
margin: isResponsiveDesktop
? const EdgeInsetsDirectional.only(end: 16)
: null,
padding: const EdgeInsets.only(top: 16),
child: const SizedBox(
width: 24,
height: 24,
child: CupertinoLoadingWidget(),
),
),
);
}
}
@@ -0,0 +1,89 @@
import 'package:collection/collection.dart';
import 'package:flutter/material.dart';
import 'package:tmail_ui_user/features/email/presentation/email_view.dart';
import 'package:model/email/email_in_thread_status.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/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';
import 'package:tmail_ui_user/features/thread_detail/presentation/widgets/thread_detail_load_more_circle.dart';
extension GetThreadDetailEmailViews on ThreadDetailController {
List<Widget> 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);
}
}
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) {
return const SizedBox.shrink();
}
return ThreadDetailLoadMoreCircle(
count: emailsToLoadMoreCount,
onTap: loadMoreThreadDetailEmails,
imagePaths: imagePaths,
isLoading: loadingThreadDetail,
);
}
if (presentationEmail.emailInThreadStatus == null) {
return const SizedBox.shrink();
}
final isFirstEmailInThreadDetail = indexOfEmailId == 0;
if (presentationEmail.emailInThreadStatus == EmailInThreadStatus.collapsed) {
return ThreadDetailCollapsedEmail(
presentationEmail: presentationEmail.copyWith(
subject: isFirstEmailInThreadDetail
? emailIdsPresentation.values.last?.subject
: null
),
showSubject: isFirstEmailInThreadDetail,
imagePaths: imagePaths,
responsiveUtils: responsiveUtils,
mailboxContain: presentationEmail.mailboxContain,
emailLoaded: null,
openEmailAddressDetailAction: (context, emailAddress) {
// TODO: Next PR
},
onToggleThreadDetailCollapseExpand: () {
toggleThreadDetailCollapeExpand(presentationEmail);
},
);
}
return Padding(
padding: const EdgeInsetsDirectional.only(bottom: 16),
child: EmailView(
key: GlobalObjectKey(presentationEmail.id?.id.value ?? ''),
isInsideThreadDetailView: true,
emailId: presentationEmail.id,
isFirstEmailInThreadDetail: isFirstEmailInThreadDetail,
threadSubject: isFirstEmailInThreadDetail
? emailIdsPresentation.values.last?.subject
: null,
onToggleThreadDetailCollapseExpand: () {
toggleThreadDetailCollapeExpand(presentationEmail);
},
),
);
}).toList();
}
}
@@ -5,6 +5,14 @@ extension HandleGetEmailIdsByThreadIdSuccess on ThreadDetailController {
void handleGetEmailIdsByThreadIdSuccess(
GetThreadByIdSuccess success,
) {
emailIds.value = success.emailIds;
if (success.emailIds.isNotEmpty) {
emailIdsPresentation.value = Map.fromEntries(success.emailIds.map(
(emailId) => MapEntry(emailId, null),
));
} else if (mailboxDashBoardController.selectedEmail.value?.id != null) {
emailIdsPresentation.value = {
mailboxDashBoardController.selectedEmail.value!.id!: null,
};
}
}
}
@@ -0,0 +1,63 @@
import 'package:collection/collection.dart';
import 'package:flutter/material.dart';
import 'package:tmail_ui_user/features/email/presentation/bindings/email_bindings.dart';
import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/model/dashboard_routes.dart';
import 'package:model/email/email_in_thread_status.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/thread_detail_controller.dart';
extension HandleGetEmailsByIdsSuccess on ThreadDetailController {
void handleGetEmailsByIdsSuccess(GetEmailsByIdsSuccess success) {
final currentRoute = mailboxDashBoardController.dashboardRoute.value;
if (currentRoute != DashboardRoutes.threadDetailed) {
return;
}
final isLoadMore = emailIdsPresentation.values.whereNotNull().isNotEmpty;
final currentScrollPosition = scrollController?.position.pixels;
final maxScrollExtent = scrollController?.position.maxScrollExtent;
final currentBottomScrollPosition = currentScrollPosition != null
&& maxScrollExtent != null
? maxScrollExtent - currentScrollPosition
: null;
for (var presentationEmail in success.presentationEmails) {
if (presentationEmail.id == null) continue;
if (presentationEmail.id == emailIdsPresentation.keys.last) {
EmailBindings(currentEmailId: presentationEmail.id).dependencies();
currentExpandedEmailId.value = presentationEmail.id;
}
emailIdsPresentation[presentationEmail.id!] = presentationEmail.copyWith(
emailInThreadStatus: presentationEmail.id == emailIdsPresentation.keys.last
? EmailInThreadStatus.expanded
: EmailInThreadStatus.collapsed,
);
}
if (!isLoadMore) return;
final currentExpandedEmailIndex = currentExpandedEmailId.value == null
? -1
: emailIdsPresentation.keys.toList().indexOf(currentExpandedEmailId.value!);
final firstLoadedMoreEmailId = success.presentationEmails.firstOrNull?.id;
final firstLoadedMoreEmailIndex = firstLoadedMoreEmailId == null
? -1
: emailIdsPresentation.keys.toList().indexOf(firstLoadedMoreEmailId);
if (currentExpandedEmailIndex == -1 || firstLoadedMoreEmailIndex == -1) {
return;
}
if (currentBottomScrollPosition != null) {
WidgetsBinding.instance.addPostFrameCallback((_) {
final newMaxScrollExtent = scrollController?.position.maxScrollExtent;
if (newMaxScrollExtent == null) return;
if (currentExpandedEmailIndex < firstLoadedMoreEmailIndex) {
return;
} else if (newMaxScrollExtent != maxScrollExtent!) {
scrollController?.jumpTo(newMaxScrollExtent - currentBottomScrollPosition);
}
});
}
}
}
@@ -0,0 +1,31 @@
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';
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/thread_detail_controller.dart';
import 'package:tmail_ui_user/features/thread_detail/presentation/utils/thread_detail_presentation_utils.dart';
extension InitializeThreadDetailEmails on ThreadDetailController {
void initializeThreadDetailEmails() {
final emailIdToLoadContent = emailIdsPresentation.keys.last;
final emailIdsToLoadMetaData = ThreadDetailPresentationUtils.getEmailIdsToLoad(
Map.from(emailIdsPresentation)..remove(emailIdToLoadContent),
);
if (accountId == null || session == null) {
consumeState(Stream.value(Left(GetEmailsByIdsFailure(
exception: NotFoundSessionException(),
))));
return;
}
consumeState(getEmailsByIdsInteractor.execute(
session!,
accountId!,
[...emailIdsToLoadMetaData, emailIdToLoadContent],
properties: EmailUtils.getPropertiesForEmailGetMethod(
session!,
accountId!,
),
));
}
}
@@ -0,0 +1,32 @@
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';
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/thread_detail_controller.dart';
import 'package:tmail_ui_user/features/thread_detail/presentation/utils/thread_detail_presentation_utils.dart';
extension LoadMoreThreadDetailEmails on ThreadDetailController {
void loadMoreThreadDetailEmails() {
if (accountId == null || session == null) {
consumeState(Stream.value(Left(GetEmailsByIdsFailure(
exception: NotFoundSessionException(),
))));
return;
}
final emailIdsToLoadMetaData = ThreadDetailPresentationUtils
.getEmailIdsToLoad(emailIdsPresentation);
if (emailIdsToLoadMetaData.isEmpty) {
return;
}
consumeState(getEmailsByIdsInteractor.execute(
session!,
accountId!,
emailIdsToLoadMetaData,
properties: EmailUtils.getPropertiesForEmailGetMethod(
session!,
accountId!,
),
));
}
}
@@ -0,0 +1,49 @@
import 'package:get/get.dart';
import 'package:model/email/presentation_email.dart';
import 'package:tmail_ui_user/features/email/presentation/bindings/email_bindings.dart';
import 'package:tmail_ui_user/features/email/presentation/controller/single_email_controller.dart';
import 'package:model/email/email_in_thread_status.dart';
import 'package:tmail_ui_user/features/thread_detail/presentation/extension/focus_thread_detail_expanded_email.dart';
import 'package:tmail_ui_user/features/thread_detail/presentation/thread_detail_controller.dart';
extension ToggleThreadDetailCollapeExpand on ThreadDetailController {
void toggleThreadDetailCollapeExpand(PresentationEmail presentationEmail) {
final emailId = presentationEmail.id;
final expansionStatus = presentationEmail.emailInThreadStatus;
if (emailId == null || expansionStatus == null) return;
if (expansionStatus == EmailInThreadStatus.expanded) {
emailIdsPresentation[emailId] = presentationEmail.copyWith(
emailInThreadStatus: EmailInThreadStatus.collapsed,
);
currentExpandedEmailId.value = null;
return;
}
final isInitialized = Get.isRegistered<SingleEmailController>(
tag: emailId.id.value,
);
if (!isInitialized) {
EmailBindings(currentEmailId: presentationEmail.id).dependencies();
} else {
focusExpandedEmail(emailId);
}
for (var key in emailIdsPresentation.keys) {
if (emailIdsPresentation[key] == null) continue;
if (key == emailId) {
emailIdsPresentation[key] = presentationEmail.copyWith(
emailInThreadStatus: EmailInThreadStatus.expanded,
);
currentExpandedEmailId.value = key;
continue;
}
if (emailIdsPresentation[key]?.emailInThreadStatus == EmailInThreadStatus.expanded) {
emailIdsPresentation[key] = emailIdsPresentation[key]?.copyWith(
emailInThreadStatus: EmailInThreadStatus.collapsed,
);
}
}
}
}