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: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 {
GetEmailsByIdsSuccess(this.presentationEmails);
@@ -22,6 +22,7 @@ class GetEmailsByIdsInteractor {
AccountId accountId,
List<EmailId> 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,
@@ -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<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);
}
}
final loadMoreSegments = Map<LoadMoreIndex, LoadMoreCount>.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,
),
);
}
@@ -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,
);
@@ -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!,
@@ -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,
));
}
}
@@ -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,
);
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
@@ -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<EmailId> getEmailIdsToLoad(
Map<EmailId, PresentationEmail?> emailIdsPresentation,
) {
final validEmailIdsToLoad = emailIdsPresentation.entries
.where((entry) => entry.value == null)
.map((entry) => entry.key)
.toList();
static List<EmailId> getFirstLoadEmailIds(
List<EmailId> 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<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,
);
}
}