TF-3941 Thread Detail Thread level mark read/star, archive and spam

This commit is contained in:
DatDang
2025-08-05 14:15:03 +07:00
committed by Dat H. Pham
parent f44118af48
commit b7cd3f7179
35 changed files with 902 additions and 552 deletions
+23
View File
@@ -0,0 +1,23 @@
# 68. Thread level actions
Date: 2025-08-18
## Status
Accepted
## Context
- When user send an email, a copy of that email is created in Sent mailbox
- If user sent an email, and recipients contains own user, there will be 2 emails in the thread, one in Sent mailbox, and another in Inbox mailbox (or the other mailbox if there is email rule for it).
- This cause the thread has 2 emails with the exact same content.
- Current solution is filtering out the email in Sent mailbox when displaying thread.
- However, the thread level actions are ignoring these filtered-out emails.
## Decision
- All thread level actions will be applied to all emails of thread.
## Consequences
- If user move emails of thread to other mailbox, there will be email duplication due to the filtering conditions are not met anymore.
@@ -46,6 +46,7 @@ extension EmailActionTypeExtension on EmailActionType {
switch(this) {
case EmailActionType.archiveMessage:
case EmailActionType.moveToMailbox:
case EmailActionType.archiveMessage:
return AppLocalizations.of(context).movedToFolder(destinationPath ?? '');
case EmailActionType.moveToTrash:
return AppLocalizations.of(context).moved_to_trash;
@@ -207,9 +207,6 @@ class SingleEmailController extends BaseController with AppLoaderMixin {
: _threadDetailController?.emailIdsPresentation[_currentEmailId];
}
bool get isOnlyEmailInThread =>
_threadDetailController?.emailIdsPresentation.length == 1;
bool get calendarEventProcessing => viewState.value.fold(
(failure) => false,
(success) => success is CalendarEventReplying);
+26 -29
View File
@@ -252,40 +252,37 @@ class EmailView extends GetWidget<SingleEmailController> {
onEmailActionClick: (presentationEmail, actionType) => controller.handleEmailAction(context, presentationEmail, actionType),
isInsideThreadDetailView: isInsideThreadDetailView,
emailLoaded: controller.currentEmailLoaded.value,
onMoreActionClick: controller.isOnlyEmailInThread
? null
: (presentationEmail, position) => controller.emailActionReactor.handleMoreEmailAction(
mailboxContain: controller.getMailboxContain(presentationEmail),
presentationEmail: presentationEmail,
position: position,
responsiveUtils: controller.responsiveUtils,
imagePaths: controller.imagePaths,
username: controller.session?.username,
handleEmailAction: (email, action) => controller.handleEmailAction(context, email, action),
additionalActions: [
EmailActionType.forward,
EmailActionType.replyAll,
EmailActionType.replyToList,
EmailActionType.printAll,
if (controller.responsiveUtils.isMobile(context))
EmailActionType.moveToMailbox,
if (!controller.responsiveUtils.isDesktop(context)) ...[
EmailActionType.markAsStarred,
EmailActionType.unMarkAsStarred,
EmailActionType.moveToTrash,
EmailActionType.deletePermanently,
],
],
emailIsRead: presentationEmail.hasRead,
openBottomSheetContextMenu: controller.mailboxDashBoardController.openBottomSheetContextMenu,
openPopupMenu: controller.mailboxDashBoardController.openPopupMenu,
),
onMoreActionClick: (presentationEmail, position) => controller.emailActionReactor.handleMoreEmailAction(
mailboxContain: controller.getMailboxContain(presentationEmail),
presentationEmail: presentationEmail,
position: position,
responsiveUtils: controller.responsiveUtils,
imagePaths: controller.imagePaths,
username: controller.session?.username,
handleEmailAction: (email, action) => controller.handleEmailAction(context, email, action),
additionalActions: [
EmailActionType.forward,
EmailActionType.replyAll,
EmailActionType.replyToList,
EmailActionType.printAll,
if (controller.responsiveUtils.isMobile(context))
EmailActionType.moveToMailbox,
if (!controller.responsiveUtils.isDesktop(context)) ...[
EmailActionType.markAsStarred,
EmailActionType.unMarkAsStarred,
EmailActionType.moveToTrash,
EmailActionType.deletePermanently,
],
],
emailIsRead: presentationEmail.hasRead,
openBottomSheetContextMenu: controller.mailboxDashBoardController.openBottomSheetContextMenu,
openPopupMenu: controller.mailboxDashBoardController.openPopupMenu,
),
onToggleThreadDetailCollapseExpand: onToggleThreadDetailCollapseExpand,
onTapAvatarActionClick: onToggleThreadDetailCollapseExpand,
mailboxContain: presentationEmail.findMailboxContain(
controller.mailboxDashBoardController.mapMailboxById,
),
isOnlyEmailInThread: controller.isOnlyEmailInThread,
)),
if (!controller.responsiveUtils.isMobile(context))
const SizedBox(height: 24),
@@ -23,6 +23,16 @@ extension EmailExtension on Email {
}
}
bool checkEmailValidForThreadDetail(
MailboxId sentMailboxId,
String ownEmailAddress) {
return id != null && (
!inSentMailbox(sentMailboxId)
|| !fromMe(ownEmailAddress)
|| !recipientsHasMe(ownEmailAddress)
);
}
bool inSentMailbox(MailboxId sentMailboxId) {
return mailboxIds?[sentMailboxId] == true;
}
@@ -35,7 +35,6 @@ class EmailViewAppBarWidget extends StatelessWidget {
final EdgeInsetsGeometry? iconPadding;
final EdgeInsetsGeometry? iconMargin;
final EdgeInsetsGeometry? padding;
final bool isOnlyEmailInThread;
EmailViewAppBarWidget({
Key? key,
@@ -53,7 +52,6 @@ class EmailViewAppBarWidget extends StatelessWidget {
this.iconPadding,
this.iconMargin,
this.padding,
this.isOnlyEmailInThread = false,
}) : super(key: key);
@override
@@ -266,8 +264,7 @@ class EmailViewAppBarWidget extends StatelessWidget {
_getMarkStarButton(appLocalizations),
_getDeleteButton(appLocalizations),
],
if (!isOnlyEmailInThread)
_getMoreButton(appLocalizations, isScreenWithShortestSide),
_getMoreButton(appLocalizations, isScreenWithShortestSide),
];
}
}
@@ -44,7 +44,6 @@ class InformationSenderAndReceiverBuilder extends StatelessWidget {
final OnTapAvatarActionClick? onTapAvatarActionClick;
final PresentationMailbox? mailboxContain;
final bool showUnreadVisualization;
final bool isOnlyEmailInThread;
const InformationSenderAndReceiverBuilder({
Key? key,
@@ -64,7 +63,6 @@ class InformationSenderAndReceiverBuilder extends StatelessWidget {
this.onTapAvatarActionClick,
this.mailboxContain,
this.showUnreadVisualization = false,
this.isOnlyEmailInThread = false,
}) : super(key: key);
@override
@@ -201,7 +199,6 @@ class InformationSenderAndReceiverBuilder extends StatelessWidget {
iconPadding: const EdgeInsets.all(8),
iconMargin: EdgeInsets.zero,
padding: EdgeInsets.zero,
isOnlyEmailInThread: isOnlyEmailInThread,
),
),
),
@@ -22,6 +22,7 @@ import 'package:jmap_dart_client/jmap/core/unsigned_int.dart';
import 'package:jmap_dart_client/jmap/identities/identity.dart';
import 'package:jmap_dart_client/jmap/mail/email/email.dart';
import 'package:jmap_dart_client/jmap/mail/email/email_address.dart';
import 'package:jmap_dart_client/jmap/mail/email/keyword_identifier.dart';
import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart';
import 'package:jmap_dart_client/jmap/mail/vacation/vacation_response.dart';
import 'package:model/model.dart';
@@ -181,6 +182,7 @@ import 'package:tmail_ui_user/features/thread/domain/usecases/mark_as_multiple_e
import 'package:tmail_ui_user/features/thread/domain/usecases/mark_as_star_multiple_email_interactor.dart';
import 'package:tmail_ui_user/features/thread/domain/usecases/move_multiple_email_to_mailbox_interactor.dart';
import 'package:tmail_ui_user/features/thread/presentation/model/delete_action_type.dart';
import 'package:tmail_ui_user/features/thread_detail/domain/model/email_in_thread_detail_info.dart';
import 'package:tmail_ui_user/features/thread_detail/presentation/action/thread_detail_ui_action.dart';
import 'package:tmail_ui_user/main/deep_links/deep_link_data.dart';
import 'package:tmail_ui_user/main/deep_links/deep_links_manager.dart';
@@ -1563,6 +1565,54 @@ class MailboxDashBoardController extends ReloadableController
);
}
void moveMultipleEmailInThreadDetail(
List<EmailInThreadDetailInfo> emailsInThreadDetailInfo, {
required MailboxId destinationMailboxId,
required EmailActionType emailActionType,
}) {
if (sessionCurrent == null || accountId.value == null) {
consumeState(Stream.value(
Left(MoveMultipleEmailToMailboxFailure(
emailActionType,
MoveAction.moving,
NotFoundSessionException()
))
));
return;
}
final currentMailboxes = <MailboxId, List<EmailId>>{};
for (final email in emailsInThreadDetailInfo) {
final mailboxIdContain = email.mailboxIdContain;
if (mailboxIdContain == null) continue;
currentMailboxes.putIfAbsent(mailboxIdContain, () => []).add(email.emailId);
}
final moveRequest = MoveToMailboxRequest(
currentMailboxes,
destinationMailboxId,
MoveAction.moving,
emailActionType,
destinationPath: currentContext == null
? mapMailboxById[destinationMailboxId]?.name?.name
: mapMailboxById[destinationMailboxId]?.getDisplayName(currentContext!),
);
final emailIdsWithReadStatus = Map.fromEntries(
emailsInThreadDetailInfo.map(
(email) => MapEntry(
email.emailId,
email.keywords?[KeyWordIdentifier.emailSeen] == true,
),
),
);
_moveSelectedEmailMultipleToMailboxAction(
sessionCurrent!,
accountId.value!,
moveRequest,
emailIdsWithReadStatus,
);
}
void deleteSelectionEmailsPermanently(
BuildContext context,
DeleteActionType actionType,
@@ -3,11 +3,14 @@ import 'package:jmap_dart_client/jmap/account_id.dart';
import 'package:jmap_dart_client/jmap/core/id.dart';
import 'package:jmap_dart_client/jmap/core/user_name.dart';
import 'package:jmap_dart_client/jmap/mail/email/email.dart';
import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart';
import 'package:model/extensions/account_id_extensions.dart';
import 'package:model/extensions/email_id_extensions.dart';
import 'package:tmail_ui_user/features/caching/utils/cache_utils.dart';
import 'package:tmail_ui_user/features/email/presentation/extensions/email_extension.dart';
import 'package:tmail_ui_user/features/thread/data/extensions/email_extension.dart';
import 'package:tmail_ui_user/features/thread/data/model/email_cache.dart';
import 'package:tmail_ui_user/features/thread_detail/domain/model/email_in_thread_detail_info.dart';
extension ListEmailExtension on List<Email> {
Map<String, EmailCache> toMapCache(AccountId accountId, UserName userName) {
@@ -39,4 +42,23 @@ extension ListEmailExtension on List<Email> {
return this;
}
List<EmailInThreadDetailInfo> toEmailsInThreadDetailInfo({
required MailboxId? sentMailboxId,
required String? ownEmailAddress,
}) {
return map(
(email) => EmailInThreadDetailInfo(
emailId: email.id!,
keywords: email.keywords,
mailboxIds: email.mailboxIds,
isValidToDisplay: sentMailboxId == null || ownEmailAddress == null
? true
: email.checkEmailValidForThreadDetail(
sentMailboxId,
ownEmailAddress,
),
),
).toList();
}
}
@@ -4,7 +4,9 @@ import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart';
import 'package:model/email/presentation_email.dart';
import 'package:model/extensions/presentation_email_extension.dart';
import 'package:model/mailbox/presentation_mailbox.dart';
import 'package:tmail_ui_user/features/email/presentation/extensions/email_extension.dart';
import 'package:tmail_ui_user/features/thread/domain/model/search_query.dart';
import 'package:tmail_ui_user/features/thread_detail/domain/model/email_in_thread_detail_info.dart';
import 'package:tmail_ui_user/main/routes/app_routes.dart';
import 'package:tmail_ui_user/main/routes/navigation_router.dart';
import 'package:tmail_ui_user/main/routes/route_utils.dart';
@@ -56,4 +58,23 @@ extension ListPresentationEmailExtensions on List<PresentationEmail> {
return null;
}
}
List<EmailInThreadDetailInfo> toEmailsInThreadDetailInfo({
required MailboxId? sentMailboxId,
required String? ownEmailAddress,
}) {
return map(
(email) => EmailInThreadDetailInfo(
emailId: email.id!,
keywords: email.keywords,
mailboxIds: email.mailboxIds,
isValidToDisplay: sentMailboxId == null || ownEmailAddress == null
? true
: email.toEmail().checkEmailValidForThreadDetail(
sentMailboxId,
ownEmailAddress,
),
),
).toList();
}
}
@@ -12,6 +12,7 @@ import 'package:model/extensions/list_email_extension.dart';
import 'package:tmail_ui_user/features/email/presentation/extensions/email_extension.dart';
import 'package:tmail_ui_user/features/thread_detail/data/data_source/thread_detail_data_source.dart';
import 'package:tmail_ui_user/features/thread_detail/domain/exceptions/empty_thread_detail_exception.dart';
import 'package:tmail_ui_user/features/thread_detail/domain/model/email_in_thread_detail_info.dart';
import 'package:tmail_ui_user/features/thread_detail/domain/repository/thread_detail_repository.dart';
class ThreadDetailRepositoryImpl implements ThreadDetailRepository {
@@ -20,14 +21,13 @@ class ThreadDetailRepositoryImpl implements ThreadDetailRepository {
final Map<DataSourceType, ThreadDetailDataSource> threadDetailDataSource;
@override
Future<List<EmailId>> getThreadById(
Future<List<EmailInThreadDetailInfo>> getThreadById(
ThreadId threadId,
Session session,
AccountId accountId,
MailboxId sentMailboxId,
String ownEmailAddress, {
required EmailId? selectedEmailId,
}) async {
String ownEmailAddress,
) async {
final originalEmailIds = await threadDetailDataSource[DataSourceType.network]!
.getThreadById(threadId, accountId);
@@ -44,27 +44,24 @@ class ThreadDetailRepositoryImpl implements ThreadDetailRepository {
emailIds,
sentMailboxId,
ownEmailAddress,
selectedEmailId: selectedEmailId,
))
);
return filteredEmailIds
.reduce((prev, curr) => prev + curr)
.reduce((prev, curr) => prev..addAll(curr))
.sortWithResult(EmailComparator(
EmailComparatorProperty.receivedAt
)..setIsAscending(true))
.map((e) => e.id!)
.toList();
.toEmailsInThreadDetailInfo();
}
Future<List<Email>> _filterBadEmails(
Future<Map<Email, bool>> _filterBadEmails(
Session session,
AccountId accountId,
List<EmailId> emailIds,
MailboxId sentMailboxId,
String ownEmailAddress, {
required EmailId? selectedEmailId,
}) async {
String ownEmailAddress,
) async {
int retry = 3;
while (retry > 0) {
try {
@@ -81,37 +78,23 @@ class ThreadDetailRepositoryImpl implements ThreadDetailRepository {
EmailProperty.cc,
EmailProperty.bcc,
EmailProperty.receivedAt,
EmailProperty.keywords,
}),
);
return emails
.where((email) => checkEmailValidForThreadDetail(
email,
sentMailboxId,
ownEmailAddress,
selectedEmailId: selectedEmailId,
))
.toList();
return {
for (final email in emails)
email: email.checkEmailValidForThreadDetail(
sentMailboxId,
ownEmailAddress,
),
};
} catch (e) {
retry--;
if (retry <= 0) rethrow;
}
}
return [];
}
bool checkEmailValidForThreadDetail(
Email email,
MailboxId sentMailboxId,
String ownEmailAddress, {
required EmailId? selectedEmailId
}) {
return email.id != null && (
!email.inSentMailbox(sentMailboxId)
|| !email.fromMe(ownEmailAddress)
|| !email.recipientsHasMe(ownEmailAddress)
|| email.id == selectedEmailId
);
return {};
}
@override
@@ -131,3 +114,27 @@ class ThreadDetailRepositoryImpl implements ThreadDetailRepository {
.getThreadDetailStatus();
}
}
extension SortListEmailInThreadDetailInfo on Map<Email, bool> {
Map<Email, bool> sortWithResult(
EmailComparator comparator,
) {
final originalEmails = keys.toList();
final sortedEmails = originalEmails.sortWithResult(comparator);
return {
for (final email in sortedEmails)
email: this[email] ?? false,
};
}
List<EmailInThreadDetailInfo> toEmailsInThreadDetailInfo() {
return entries.map(
(entry) => EmailInThreadDetailInfo(
emailId: entry.key.id!,
keywords: entry.key.keywords,
mailboxIds: entry.key.mailboxIds,
isValidToDisplay: entry.value,
),
).toList();
}
}
@@ -0,0 +1,9 @@
import 'package:jmap_dart_client/jmap/mail/email/email.dart';
import 'package:tmail_ui_user/features/thread_detail/domain/model/email_in_thread_detail_info.dart';
extension ListEmailInThreadDetailInfoExtension on List<EmailInThreadDetailInfo> {
List<EmailId> get emailIdsToDisplay =>
where((email) => email.isValidToDisplay)
.map((email) => email.emailId)
.toList();
}
@@ -0,0 +1,42 @@
import 'package:collection/collection.dart';
import 'package:equatable/equatable.dart';
import 'package:jmap_dart_client/jmap/mail/email/email.dart';
import 'package:jmap_dart_client/jmap/mail/email/keyword_identifier.dart';
import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart';
class EmailInThreadDetailInfo with EquatableMixin {
const EmailInThreadDetailInfo({
required this.emailId,
required this.keywords,
required this.mailboxIds,
required this.isValidToDisplay,
});
final EmailId emailId;
final Map<KeyWordIdentifier, bool>? keywords;
final Map<MailboxId, bool>? mailboxIds;
final bool isValidToDisplay;
@override
List<Object?> get props => [emailId, keywords, mailboxIds, isValidToDisplay];
EmailInThreadDetailInfo copyWith({
EmailId? emailId,
Map<KeyWordIdentifier, bool>? keywords,
Map<MailboxId, bool>? mailboxIds,
bool? isValidToDisplay,
}) {
return EmailInThreadDetailInfo(
emailId: emailId ?? this.emailId,
keywords: keywords ?? this.keywords,
mailboxIds: mailboxIds ?? this.mailboxIds,
isValidToDisplay: isValidToDisplay ?? this.isValidToDisplay,
);
}
MailboxId? get mailboxIdContain {
return mailboxIds?.entries
.firstWhereOrNull((entry) => entry.value)
?.key;
}
}
@@ -3,16 +3,16 @@ import 'package:jmap_dart_client/jmap/core/properties/properties.dart';
import 'package:jmap_dart_client/jmap/core/session/session.dart';
import 'package:jmap_dart_client/jmap/mail/email/email.dart';
import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart';
import 'package:tmail_ui_user/features/thread_detail/domain/model/email_in_thread_detail_info.dart';
abstract class ThreadDetailRepository {
Future<List<EmailId>> getThreadById(
Future<List<EmailInThreadDetailInfo>> getThreadById(
ThreadId threadId,
Session session,
AccountId accountId,
MailboxId sentMailboxId,
String ownEmailAddress, {
required EmailId? selectedEmailId,
});
String ownEmailAddress,
);
Future<List<Email>> getEmailsByIds(
Session session,
@@ -1,6 +1,7 @@
import 'package:core/presentation/state/failure.dart';
import 'package:core/presentation/state/success.dart';
import 'package:jmap_dart_client/jmap/mail/email/email.dart';
import 'package:tmail_ui_user/features/thread_detail/domain/model/email_in_thread_detail_info.dart';
class GettingThreadById extends LoadingState {
final bool updateCurrentThreadDetail;
@@ -16,25 +17,32 @@ class GetThreadByIdSuccess extends UIState {
final ThreadId? threadId;
final bool updateCurrentThreadDetail;
final bool skipLoadingMetadata;
final List<EmailInThreadDetailInfo> emailsInThreadDetailInfo;
GetThreadByIdSuccess(
this.emailIds, {
required this.threadId,
this.updateCurrentThreadDetail = false,
this.skipLoadingMetadata = false,
this.emailsInThreadDetailInfo = const [],
});
@override
List<Object?> get props => [
emailIds,
threadId,
emailsInThreadDetailInfo,
updateCurrentThreadDetail,
skipLoadingMetadata,
];
}
class PreloadEmailIdsInThreadSuccess extends GetThreadByIdSuccess {
PreloadEmailIdsInThreadSuccess(super.emailIds, {required super.threadId});
PreloadEmailIdsInThreadSuccess(
super.emailIds, {
required super.threadId,
super.emailsInThreadDetailInfo,
});
}
class GetThreadByIdFailure extends FeatureFailure {
@@ -6,6 +6,7 @@ import 'package:jmap_dart_client/jmap/account_id.dart';
import 'package:jmap_dart_client/jmap/core/session/session.dart';
import 'package:jmap_dart_client/jmap/mail/email/email.dart';
import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart';
import 'package:tmail_ui_user/features/thread_detail/domain/extensions/list_email_in_thread_detail_info_extension.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_thread_by_id_state.dart';
@@ -20,7 +21,6 @@ class GetThreadByIdInteractor {
AccountId accountId,
MailboxId sentMailboxId,
String ownEmailAddress, {
required EmailId? selectedEmailId,
bool updateCurrentThreadDetail = false,
}) async* {
try {
@@ -33,13 +33,13 @@ class GetThreadByIdInteractor {
accountId,
sentMailboxId,
ownEmailAddress,
selectedEmailId: selectedEmailId,
);
yield Right(GetThreadByIdSuccess(
result,
result.emailIdsToDisplay,
threadId: threadId,
updateCurrentThreadDetail: updateCurrentThreadDetail,
emailsInThreadDetailInfo: result,
));
} catch (e) {
logError('GetEmailIdsByThreadIdInteractor::execute(): Exception: $e');
@@ -51,7 +51,6 @@ class GetThreadByIdInteractor {
accountId,
sentMailboxId,
ownEmailAddress,
selectedEmailId: selectedEmailId,
),
updateCurrentThreadDetail: updateCurrentThreadDetail,
));
@@ -0,0 +1,49 @@
import 'package:jmap_dart_client/jmap/mail/email/keyword_identifier.dart';
import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart';
import 'package:model/mailbox/presentation_mailbox.dart';
import 'package:tmail_ui_user/features/thread_detail/presentation/thread_detail_controller.dart';
extension GetThreadDetailActionStatus on ThreadDetailController {
bool get threadDetailIsStarred {
return emailsInThreadDetailInfo.every(
(email) {
return email.keywords?.containsKey(KeyWordIdentifier.emailFlagged) ==
true;
},
);
}
bool get threadDetailIsRead {
return emailsInThreadDetailInfo.every(
(email) {
return email.keywords?.containsKey(KeyWordIdentifier.emailSeen) == true;
},
);
}
MailboxId? getMailboxIdByRole(Role role) {
return mailboxDashBoardController.mapDefaultMailboxIdByRole[role];
}
bool get threadDetailIsArchived {
final archiveMailboxId = getMailboxIdByRole(
PresentationMailbox.roleArchive,
);
return emailsInThreadDetailInfo.every(
(email) {
return email.mailboxIds?[archiveMailboxId] == true;
},
);
}
bool get threadDetailIsSpam {
final spamMailboxId =
getMailboxIdByRole(PresentationMailbox.roleJunk) ??
getMailboxIdByRole(PresentationMailbox.roleSpam);
return emailsInThreadDetailInfo.every(
(email) {
return email.mailboxIds?[spamMailboxId] == true;
},
);
}
}
@@ -1,10 +1,7 @@
import 'package:flutter/material.dart';
import 'package:model/email/email_action_type.dart';
import 'package:model/extensions/presentation_email_extension.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/mailbox_dashboard/presentation/extensions/handle_open_context_menu_extension.dart';
import 'package:tmail_ui_user/features/thread_detail/presentation/extension/get_thread_detail_email_mailbox_contains.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_open_email_address_detail_action.dart';
@@ -14,7 +11,6 @@ import 'package:tmail_ui_user/features/thread_detail/presentation/extension/togg
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';
import 'package:tmail_ui_user/main/routes/route_navigation.dart';
extension GetThreadDetailEmailViews on ThreadDetailController {
List<Widget> getThreadDetailEmailViews() {
@@ -65,33 +61,6 @@ extension GetThreadDetailEmailViews on ThreadDetailController {
),
emailLoaded: null,
onEmailActionClick: threadDetailOnEmailActionClick,
onMoreActionClick: (presentationEmail, position) => emailActionReactor.handleMoreEmailAction(
mailboxContain: getThreadDetailEmailMailboxContains(presentationEmail),
presentationEmail: presentationEmail,
position: position,
responsiveUtils: responsiveUtils,
imagePaths: imagePaths,
username: session?.username,
handleEmailAction: threadDetailOnEmailActionClick,
additionalActions: [
EmailActionType.forward,
EmailActionType.replyAll,
EmailActionType.replyToList,
EmailActionType.printAll,
if (currentContext != null &&
responsiveUtils.isMobile(currentContext!))
EmailActionType.moveToMailbox,
if (!responsiveUtils.isDesktop(currentContext!)) ...[
EmailActionType.markAsStarred,
EmailActionType.unMarkAsStarred,
EmailActionType.moveToTrash,
EmailActionType.deletePermanently,
],
],
emailIsRead: presentationEmail.hasRead,
openBottomSheetContextMenu: mailboxDashBoardController.openBottomSheetContextMenu,
openPopupMenu: mailboxDashBoardController.openPopupMenu,
),
openEmailAddressDetailAction: (_, emailAddress) {
openEmailAddressDetailAction(emailAddress);
},
@@ -11,15 +11,22 @@ extension HandleGetEmailIdsByThreadIdSuccess on ThreadDetailController {
}
final allEmailIds = success.emailIds;
final allEmailsInThreadDetailInfo = success.emailsInThreadDetailInfo;
if (success.updateCurrentThreadDetail) {
final newEmailIds = allEmailIds.where(
(emailId) => !emailIdsPresentation.keys.contains(emailId),
);
final newEmailsInThreadDetailInfo = allEmailsInThreadDetailInfo.where(
(email) => !emailsInThreadDetailInfo.contains(email),
);
emailIdsPresentation
..removeWhere((key, _) => !allEmailIds.contains(key))
..addEntries(
newEmailIds.map((emailId) => MapEntry(emailId, null)),
);
emailsInThreadDetailInfo
..removeWhere((e) => !allEmailsInThreadDetailInfo.contains(e))
..addAll(newEmailsInThreadDetailInfo);
return;
}
@@ -31,5 +38,9 @@ extension HandleGetEmailIdsByThreadIdSuccess on ThreadDetailController {
? emailIdsPresentation[id] ?? selectedEmail
: null,
};
if ((isThreadDetailEnabled && success is! PreloadEmailIdsInThreadSuccess) ||
(!isThreadDetailEnabled && success is PreloadEmailIdsInThreadSuccess)) {
emailsInThreadDetailInfo.value = allEmailsInThreadDetailInfo;
}
}
}
@@ -0,0 +1,32 @@
import 'package:core/presentation/state/success.dart';
import 'package:dartz/dartz.dart';
import 'package:jmap_dart_client/jmap/mail/email/email.dart';
import 'package:model/email/read_actions.dart';
import 'package:tmail_ui_user/features/thread_detail/presentation/extension/close_thread_detail_action.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/main/routes/route_navigation.dart';
extension HandleMarkMultipleEmailsReadSuccess on ThreadDetailController {
void handleMarkMultipleEmailsReadSuccess(
UIState success,
ReadActions readActions,
List<EmailId> emailIds,
) {
mailboxDashBoardController.consumeState(Stream.value(Right(success)));
if (readActions == ReadActions.markAsRead) return;
if (emailsInThreadDetailInfo.length == 1) {
closeThreadDetailAction(currentContext);
return;
}
final expandedEmailId = currentExpandedEmailId.value;
if (expandedEmailId == null) return;
final expandedEmail = emailIdsPresentation[expandedEmailId];
if (emailIds.contains(expandedEmailId) && expandedEmail != null) {
toggleThreadDetailCollapeExpand(expandedEmail);
}
}
}
@@ -7,6 +7,7 @@ import 'package:model/extensions/keyword_identifier_extension.dart';
import 'package:model/extensions/list_email_extension.dart';
import 'package:tmail_ui_user/features/email/presentation/action/email_ui_action.dart';
import 'package:tmail_ui_user/features/email/presentation/extensions/email_extension.dart';
import 'package:tmail_ui_user/features/thread/data/extensions/list_email_extension.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/state/get_thread_by_id_state.dart';
import 'package:tmail_ui_user/features/thread_detail/domain/usecases/get_thread_by_id_interactor.dart';
@@ -34,6 +35,10 @@ extension HandleRefreshThreadDetailAction on ThreadDetailController {
[currentEmailId],
updateCurrentThreadDetail: true,
threadId: mailboxDashBoardController.selectedEmail.value?.threadId,
emailsInThreadDetailInfo: action.emailChangeResponse.updated?.toEmailsInThreadDetailInfo(
sentMailboxId: sentMailboxId,
ownEmailAddress: ownEmailAddress,
) ?? [],
))));
}
@@ -71,6 +76,14 @@ extension HandleRefreshThreadDetailAction on ThreadDetailController {
updated: emailsUpdated,
destroyed: emailIdsDestroyed.toList(),
);
final afterRefreshedThreadDetailInfo = ThreadDetailPresentationUtils.refreshThreadDetailInfo(
original: emailsInThreadDetailInfo,
created: emailsCreated,
updated: emailsUpdated,
destroyed: emailIdsDestroyed.toList(),
sentMailboxId: sentMailboxId,
ownEmailAddress: ownEmailAddress,
);
if (afterRefreshedEmailIds.isEmpty) {
closeThreadDetailAction(currentContext);
@@ -81,6 +94,7 @@ extension HandleRefreshThreadDetailAction on ThreadDetailController {
afterRefreshedEmailIds,
threadId: currentThreadId,
updateCurrentThreadDetail: true,
emailsInThreadDetailInfo: afterRefreshedThreadDetailInfo,
))));
consumeState(Stream.value(Right(GetEmailsByIdsSuccess(
afterRefreshedEmails,
@@ -0,0 +1,160 @@
import 'package:dartz/dartz.dart';
import 'package:flutter/material.dart';
import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart';
import 'package:model/email/email_action_type.dart';
import 'package:model/email/mark_star_action.dart';
import 'package:model/email/read_actions.dart';
import 'package:model/mailbox/presentation_mailbox.dart';
import 'package:tmail_ui_user/features/base/widget/popup_menu/popup_menu_item_action_widget.dart';
import 'package:tmail_ui_user/features/email/presentation/model/context_item_email_action.dart';
import 'package:tmail_ui_user/features/email/presentation/model/popup_menu_item_email_action.dart';
import 'package:tmail_ui_user/features/home/data/exceptions/session_exceptions.dart';
import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/extensions/handle_open_context_menu_extension.dart';
import 'package:tmail_ui_user/features/thread/domain/state/mark_as_multiple_email_read_state.dart';
import 'package:tmail_ui_user/features/thread/domain/state/mark_as_star_multiple_email_state.dart';
import 'package:tmail_ui_user/features/thread_detail/presentation/extension/close_thread_detail_action.dart';
import 'package:tmail_ui_user/features/thread_detail/presentation/extension/get_thread_detail_action_status.dart';
import 'package:tmail_ui_user/features/thread_detail/presentation/thread_detail_controller.dart';
import 'package:tmail_ui_user/main/localizations/app_localizations.dart';
import 'package:tmail_ui_user/main/routes/route_navigation.dart';
extension OnThreadDetailActionClick on ThreadDetailController {
void onThreadDetailActionClick(EmailActionType threadDetailActionType) {
switch (threadDetailActionType) {
case EmailActionType.markAsRead:
case EmailActionType.markAsUnread:
if (session == null || accountId == null) {
consumeState(Stream.value(Left(MarkAsMultipleEmailReadFailure(
threadDetailActionType == EmailActionType.markAsRead
? ReadActions.markAsRead
: ReadActions.markAsUnread,
NotFoundSessionException(),
))));
return;
}
consumeState(markAsMultipleEmailReadInteractor.execute(
session!,
accountId!,
emailsInThreadDetailInfo.map((e) => e.emailId).toList(),
threadDetailActionType == EmailActionType.markAsRead
? ReadActions.markAsRead
: ReadActions.markAsUnread,
{},
));
break;
case EmailActionType.markAsStarred:
case EmailActionType.unMarkAsStarred:
if (session == null || accountId == null) {
consumeState(Stream.value(Left(MarkAsStarMultipleEmailFailure(
threadDetailActionType == EmailActionType.markAsStarred
? MarkStarAction.markStar
: MarkStarAction.unMarkStar,
NotFoundSessionException(),
))));
return;
}
consumeState(markAsStarMultipleEmailInteractor.execute(
session!,
accountId!,
emailsInThreadDetailInfo.map((e) => e.emailId).toList(),
threadDetailActionType == EmailActionType.markAsStarred
? MarkStarAction.markStar
: MarkStarAction.unMarkStar,
));
break;
case EmailActionType.archiveMessage:
final mailboxId = mailboxDashBoardController.getMailboxIdByRole(
PresentationMailbox.roleArchive,
);
if (mailboxId == null) return;
_moveToMailbox(mailboxId, threadDetailActionType);
break;
case EmailActionType.moveToSpam:
final mailboxId = mailboxDashBoardController.getMailboxIdByRole(
PresentationMailbox.roleJunk,
) ?? mailboxDashBoardController.getMailboxIdByRole(
PresentationMailbox.roleSpam,
);
if (mailboxId == null) return;
_moveToMailbox(mailboxId, threadDetailActionType);
break;
case EmailActionType.unSpam:
final mailboxId = mailboxDashBoardController.getMailboxIdByRole(
PresentationMailbox.roleInbox,
);
if (mailboxId == null) return;
_moveToMailbox(mailboxId, threadDetailActionType);
break;
default:
break;
}
}
void onThreadDetailMoreActionClick(RelativeRect? position) {
if (currentContext == null) return;
final moreActions = [
threadDetailIsRead
? EmailActionType.markAsUnread
: EmailActionType.markAsRead,
threadDetailIsStarred
? EmailActionType.unMarkAsStarred
: EmailActionType.markAsStarred,
if (!threadDetailIsArchived) EmailActionType.archiveMessage,
threadDetailIsSpam ? EmailActionType.unSpam : EmailActionType.moveToSpam,
];
if (position == null) {
mailboxDashBoardController.openBottomSheetContextMenu(
context: currentContext!,
itemActions: moreActions
.map(
(action) => ContextItemEmailAction(
action,
AppLocalizations.of(currentContext!),
imagePaths,
),
)
.toList(),
onContextMenuActionClick: (action) {
popBack();
onThreadDetailActionClick(action.action);
},
);
} else {
mailboxDashBoardController.openPopupMenu(
currentContext!,
position,
moreActions.map((action) {
return PopupMenuItem(
key: Key('${action.name}_action'),
padding: EdgeInsets.zero,
child: PopupMenuItemActionWidget(
menuAction: PopupMenuItemEmailAction(
action,
AppLocalizations.of(currentContext!),
imagePaths,
),
menuActionClick: (_) {
popBack();
onThreadDetailActionClick(action);
},
),
);
}).toList(),
);
}
}
void _moveToMailbox(MailboxId mailboxId, EmailActionType emailActionType) {
closeThreadDetailAction(currentContext);
mailboxDashBoardController.moveMultipleEmailInThreadDetail(
emailsInThreadDetailInfo,
destinationMailboxId: mailboxId,
emailActionType: emailActionType,
);
}
}
@@ -1,18 +1,10 @@
import 'dart:async';
import 'package:core/presentation/utils/html_transformer/transform_configuration.dart';
import 'package:model/email/email_action_type.dart';
import 'package:model/email/mark_star_action.dart';
import 'package:model/email/presentation_email.dart';
import 'package:model/email/read_actions.dart';
import 'package:model/extensions/presentation_email_extension.dart';
import 'package:model/extensions/session_extension.dart';
import 'package:tmail_ui_user/features/email/presentation/model/composer_arguments.dart';
import 'package:tmail_ui_user/features/email/presentation/utils/email_utils.dart';
import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/extensions/open_and_close_composer_extension.dart';
import 'package:tmail_ui_user/features/thread_detail/presentation/extension/close_thread_detail_action.dart';
import 'package:tmail_ui_user/features/thread_detail/presentation/thread_detail_controller.dart';
import 'package:tmail_ui_user/main/routes/route_navigation.dart';
extension ThreadDetailOnEmailActionClick on ThreadDetailController {
void threadDetailOnEmailActionClick(
@@ -23,60 +15,9 @@ extension ThreadDetailOnEmailActionClick on ThreadDetailController {
case EmailActionType.markAsRead:
_markRead(presentationEmail);
break;
case EmailActionType.markAsUnread:
_unRead(presentationEmail);
break;
case EmailActionType.markAsStarred:
_markStar(presentationEmail, MarkStarAction.markStar);
break;
case EmailActionType.unMarkAsStarred:
_markStar(presentationEmail, MarkStarAction.unMarkStar);
break;
case EmailActionType.moveToMailbox:
_moveEmail(presentationEmail);
break;
case EmailActionType.moveToTrash:
_trashEmail(presentationEmail);
break;
case EmailActionType.deletePermanently:
_deleteEmailPermanently(presentationEmail);
break;
case EmailActionType.moveToSpam:
_markEmailSpam(presentationEmail);
break;
case EmailActionType.unSpam:
_unSpamEmail(presentationEmail);
break;
case EmailActionType.createRule:
_quickCreateRule(presentationEmail);
break;
case EmailActionType.unsubscribe:
_unsubscribeEmail(presentationEmail);
break;
case EmailActionType.archiveMessage:
_archiveMessage(presentationEmail);
break;
case EmailActionType.printAll:
_printEmail(presentationEmail);
break;
case EmailActionType.downloadMessageAsEML:
_downloadMessageAsEML(presentationEmail);
break;
case EmailActionType.editAsNewEmail:
_editAsNewEmail(presentationEmail);
break;
case EmailActionType.reply:
_replyEmail(presentationEmail);
break;
case EmailActionType.replyAll:
_replyAll(presentationEmail);
break;
case EmailActionType.replyToList:
_replyToList(presentationEmail);
break;
case EmailActionType.forward:
_forward(presentationEmail);
break;
default:
break;
}
@@ -93,212 +34,6 @@ extension ThreadDetailOnEmailActionClick on ThreadDetailController {
));
}
void _unRead(PresentationEmail presentationEmail) {
if (session == null || accountId == null || !presentationEmail.hasRead) return;
consumeState(emailActionReactor.markAsEmailRead(
session!,
accountId!,
presentationEmail,
readAction: ReadActions.markAsUnread,
));
}
void _markStar(
PresentationEmail presentationEmail,
MarkStarAction markStarAction,
) {
if (session == null ||
accountId == null ||
(presentationEmail.hasStarred && markStarAction == MarkStarAction.markStar) ||
(!presentationEmail.hasStarred && markStarAction == MarkStarAction.unMarkStar)) {
return;
}
consumeState(emailActionReactor.markAsStarEmail(
session!,
accountId!,
presentationEmail,
markStarAction: markStarAction,
));
}
Future<void> _moveEmail(PresentationEmail presentationEmail) async {
if (session == null || accountId == null) return;
final moveActionRequest = await emailActionReactor.moveToMailbox(
session!,
accountId!,
presentationEmail,
mapMailbox: mailboxDashBoardController.mapMailboxById,
selectedMailbox: presentationEmail.findMailboxContain(
mailboxDashBoardController.mapMailboxById,
),
isSearchEmailRunning: mailboxDashBoardController.searchController.isSearchEmailRunning,
);
if (moveActionRequest == null) return;
mailboxDashBoardController.moveToMailbox(
session!,
accountId!,
moveActionRequest.moveRequest,
moveActionRequest.emailIdsWithReadStatus,
);
if (emailIdsPresentation.length == 1) closeThreadDetailAction(currentContext);
}
void _trashEmail(PresentationEmail presentationEmail) {
if (session == null || accountId == null) return;
final moveActionRequest = emailActionReactor.moveToTrash(
presentationEmail,
mapMailbox: mailboxDashBoardController.mapMailboxById,
selectedMailbox: mailboxDashBoardController.selectedMailbox.value,
isSearchEmailRunning: mailboxDashBoardController.searchController.isSearchEmailRunning,
mapDefaultMailboxIdByRole: mailboxDashBoardController.mapDefaultMailboxIdByRole,
);
if (moveActionRequest == null) return;
mailboxDashBoardController.moveToMailbox(
session!,
accountId!,
moveActionRequest.moveRequest,
moveActionRequest.emailIdsWithReadStatus,
);
if (emailIdsPresentation.length == 1) closeThreadDetailAction(currentContext);
}
void _markEmailSpam(PresentationEmail presentationEmail) {
if (session == null || accountId == null) return;
final moveActionRequest = emailActionReactor.moveToSpam(
presentationEmail,
mapMailbox: mailboxDashBoardController.mapMailboxById,
selectedMailbox: mailboxDashBoardController.selectedMailbox.value,
isSearchEmailRunning: mailboxDashBoardController.searchController.isSearchEmailRunning,
mapDefaultMailboxIdByRole: mailboxDashBoardController.mapDefaultMailboxIdByRole,
);
if (moveActionRequest == null) return;
mailboxDashBoardController.moveToMailbox(
session!,
accountId!,
moveActionRequest.moveRequest,
moveActionRequest.emailIdsWithReadStatus,
);
if (emailIdsPresentation.length == 1) closeThreadDetailAction(currentContext);
}
void _unSpamEmail(PresentationEmail presentationEmail) {
if (session == null || accountId == null) return;
final moveActionRequest = emailActionReactor.unSpam(
presentationEmail,
mapMailbox: mailboxDashBoardController.mapMailboxById,
selectedMailbox: mailboxDashBoardController.selectedMailbox.value,
isSearchEmailRunning: mailboxDashBoardController.searchController.isSearchEmailRunning,
mapDefaultMailboxIdByRole: mailboxDashBoardController.mapDefaultMailboxIdByRole,
);
if (moveActionRequest == null) return;
mailboxDashBoardController.moveToMailbox(
session!,
accountId!,
moveActionRequest.moveRequest,
moveActionRequest.emailIdsWithReadStatus,
);
if (emailIdsPresentation.length == 1) closeThreadDetailAction(currentContext);
}
void _deleteEmailPermanently(PresentationEmail email) {
emailActionReactor.deleteEmailPermanently(
email,
onDeleteEmailRequest: (email) {
popBack();
mailboxDashBoardController.deleteEmailPermanently(email);
},
responsiveUtils: responsiveUtils,
imagePaths: imagePaths,
);
if (emailIdsPresentation.length == 1) closeThreadDetailAction(currentContext);
}
void _quickCreateRule(PresentationEmail presentationEmail) {
final emailAddress = presentationEmail.from?.first;
if (session == null || accountId == null || emailAddress == null) return;
consumeState(emailActionReactor.quickCreateRule(
session!,
accountId!,
emailAddress: emailAddress,
));
}
void _unsubscribeEmail(PresentationEmail presentationEmail) {
emailActionReactor.unsubscribeEmail(
presentationEmail,
emailUnsubscribe: EmailUtils.parsingUnsubscribe(
presentationEmail.listUnsubscribe ?? '',
),
onUnsubscribeByHttpsLink: mailboxDashBoardController.unsubscribeMail,
onUnsubscribeByMailtoLink: (emailId, navigationRouter) {
mailboxDashBoardController.openComposer(
ComposerArguments.fromUnsubscribeMailtoLink(
listEmailAddress: navigationRouter.listEmailAddress,
subject: navigationRouter.subject,
body: navigationRouter.body,
previousEmailId: emailId,
)
);
},
);
}
void _archiveMessage(PresentationEmail presentationEmail) {
if (currentContext == null) return;
emailActionReactor.archiveMessage(
presentationEmail,
onArchiveEmailRequest: (presentationEmail) {
mailboxDashBoardController.archiveMessage(currentContext!, presentationEmail);
},
);
}
void _printEmail(PresentationEmail presentationEmail) {
consumeState(emailActionReactor.printEmail(
presentationEmail,
ownEmailAddress: mailboxDashBoardController.ownEmailAddress.value,
emailLoaded: null,
session: session,
accountId: accountId,
baseDownloadUrl: session?.getDownloadUrl(jmapUrl: dynamicUrlInterceptors.jmapUrl),
transformConfiguration: TransformConfiguration.forPreviewEmailOnWeb(),
));
}
void _downloadMessageAsEML(PresentationEmail presentationEmail) {
if (accountId == null || session == null) return;
consumeState(emailActionReactor.downloadMessageAsEML(
session!,
accountId!,
presentationEmail,
downloadProgressStateController: downloadProgressState,
));
}
void _editAsNewEmail(PresentationEmail presentationEmail) {
if (accountId == null || session == null) return;
emailActionReactor.editAsNewEmail(
presentationEmail,
onEditAsEmailRequest: (presentationEmail) {
mailboxDashBoardController.openComposer(
ComposerArguments.editAsNewEmail(presentationEmail),
);
},
);
}
void _replyEmail(PresentationEmail presentationEmail) {
emailActionReactor.replyEmail(
presentationEmail,
@@ -318,61 +53,4 @@ extension ThreadDetailOnEmailActionClick on ThreadDetailController {
emailLoaded: null,
);
}
void _replyAll(PresentationEmail presentationEmail) {
emailActionReactor.replyAll(
presentationEmail,
onReplyAllRequest: (presentationEmail, emailLoaded) {
mailboxDashBoardController.openComposer(
ComposerArguments.replyAllEmail(
presentationEmail: presentationEmail,
content: emailLoaded?.htmlContent,
inlineImages: emailLoaded?.inlineImages,
mailboxRole: presentationEmail.mailboxContain?.role,
messageId: presentationEmail.messageId,
references: presentationEmail.references,
listPost: presentationEmail.listPost,
)
);
},
emailLoaded: null,
);
}
void _replyToList(PresentationEmail presentationEmail) {
emailActionReactor.replyToList(
presentationEmail,
onReplyToListRequest: (presentationEmail, emailLoaded) {
mailboxDashBoardController.openComposer(
ComposerArguments.replyToListEmail(
presentationEmail: presentationEmail,
content: emailLoaded?.htmlContent,
inlineImages: emailLoaded?.inlineImages,
mailboxRole: presentationEmail.mailboxContain?.role,
messageId: presentationEmail.messageId,
references: presentationEmail.references,
listPost: presentationEmail.listPost,
)
);
},
emailLoaded: null,
);
}
void _forward(PresentationEmail presentationEmail) {
emailActionReactor.forward(
presentationEmail,
onForwardRequest: (presentationEmail, emailLoaded) {
mailboxDashBoardController.openComposer(
ComposerArguments.forwardEmail(
presentationEmail: presentationEmail,
content: emailLoaded?.htmlContent,
attachments: emailLoaded?.attachments,
inlineImages: emailLoaded?.inlineImages,
messageId: presentationEmail.messageId,
references: presentationEmail.references,
)
);
},
emailLoaded: null,
);
}
}
@@ -2,6 +2,7 @@ import 'package:dartz/dartz.dart';
import 'package:flutter/widgets.dart';
import 'package:model/email/presentation_email.dart';
import 'package:tmail_ui_user/features/email/presentation/action/email_ui_action.dart';
import 'package:tmail_ui_user/features/thread/presentation/extensions/list_presentation_email_extensions.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/state/get_thread_by_id_state.dart';
import 'package:tmail_ui_user/features/thread_detail/domain/usecases/get_thread_by_id_interactor.dart';
@@ -56,6 +57,10 @@ extension ThreadDetailOnSelectedEmailUpdated on ThreadDetailController {
Right(PreloadEmailIdsInThreadSuccess(
[selectedEmail.id!],
threadId: selectedEmail.threadId,
emailsInThreadDetailInfo: [selectedEmail].toEmailsInThreadDetailInfo(
sentMailboxId: sentMailboxId,
ownEmailAddress: ownEmailAddress,
),
)),
Right(PreloadEmailsByIdsSuccess([selectedEmail])),
]));
@@ -8,6 +8,8 @@ import 'package:tmail_ui_user/features/email/domain/usecases/mark_as_email_read_
import 'package:tmail_ui_user/features/email/domain/usecases/mark_as_star_email_interactor.dart';
import 'package:tmail_ui_user/features/email/domain/usecases/print_email_interactor.dart';
import 'package:tmail_ui_user/features/manage_account/data/local/local_setting_cache_manager.dart';
import 'package:tmail_ui_user/features/thread/domain/usecases/mark_as_multiple_email_read_interactor.dart';
import 'package:tmail_ui_user/features/thread/domain/usecases/mark_as_star_multiple_email_interactor.dart';
import 'package:tmail_ui_user/features/thread_detail/data/data_source/thread_detail_data_source.dart';
import 'package:tmail_ui_user/features/thread_detail/data/data_source/thread_detail_local_data_source_impl.dart';
import 'package:tmail_ui_user/features/thread_detail/data/data_source/thread_detail_remote_data_source_impl.dart';
@@ -43,6 +45,8 @@ class ThreadDetailBindings extends BaseBindings {
Get.find<PrintEmailInteractor>(),
Get.find<GetEmailContentInteractor>(),
Get.find<DownloadAttachmentForWebInteractor>(),
Get.find<MarkAsStarMultipleEmailInteractor>(),
Get.find<MarkAsMultipleEmailReadInteractor>(),
));
}
@@ -24,7 +24,6 @@ import 'package:tmail_ui_user/features/email/presentation/action/email_ui_action
import 'package:tmail_ui_user/features/email/presentation/model/email_loaded.dart';
import 'package:tmail_ui_user/features/email/domain/state/download_attachment_for_web_state.dart';
import 'package:tmail_ui_user/features/email/domain/state/mark_as_email_read_state.dart';
import 'package:tmail_ui_user/features/email/domain/state/mark_as_email_star_state.dart';
import 'package:tmail_ui_user/features/email/domain/usecases/download_attachment_for_web_interactor.dart';
import 'package:tmail_ui_user/features/email/domain/usecases/get_email_content_interactor.dart';
import 'package:tmail_ui_user/features/email/domain/usecases/mark_as_email_read_interactor.dart';
@@ -37,6 +36,11 @@ import 'package:tmail_ui_user/features/manage_account/domain/usecases/create_new
import 'package:tmail_ui_user/features/network_connection/presentation/network_connection_controller.dart'
if (dart.library.html) 'package:tmail_ui_user/features/network_connection/presentation/web_network_connection_controller.dart';
import 'package:tmail_ui_user/features/search/email/presentation/search_email_controller.dart';
import 'package:tmail_ui_user/features/thread/domain/state/mark_as_multiple_email_read_state.dart';
import 'package:tmail_ui_user/features/thread/domain/state/mark_as_star_multiple_email_state.dart';
import 'package:tmail_ui_user/features/thread/domain/usecases/mark_as_multiple_email_read_interactor.dart';
import 'package:tmail_ui_user/features/thread/domain/usecases/mark_as_star_multiple_email_interactor.dart';
import 'package:tmail_ui_user/features/thread_detail/domain/model/email_in_thread_detail_info.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/domain/state/get_emails_by_ids_state.dart';
import 'package:tmail_ui_user/features/thread_detail/domain/usecases/get_thread_by_id_interactor.dart';
@@ -46,6 +50,7 @@ import 'package:tmail_ui_user/features/thread_detail/presentation/extension/hand
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/extension/handle_get_emails_by_ids_success.dart';
import 'package:tmail_ui_user/features/thread_detail/presentation/extension/handle_get_thread_by_id_failure.dart';
import 'package:tmail_ui_user/features/thread_detail/presentation/extension/handle_mark_multiple_emails_read_success.dart';
import 'package:tmail_ui_user/features/thread_detail/presentation/extension/handle_refresh_thread_detail_action.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/thread_detail_on_selected_email_updated.dart';
@@ -53,7 +58,6 @@ import 'package:tmail_ui_user/features/thread_detail/presentation/thread_detail_
import 'package:tmail_ui_user/main/localizations/app_localizations.dart';
import 'package:tmail_ui_user/main/routes/route_navigation.dart';
import 'package:tmail_ui_user/features/thread_detail/presentation/extension/handle_collapsed_email_download_states.dart';
import 'package:tmail_ui_user/features/thread_detail/presentation/extension/mark_collapsed_email_star_success.dart';
import 'package:tmail_ui_user/features/thread_detail/presentation/extension/mark_collapsed_email_unread_success.dart';
import 'package:tmail_ui_user/features/thread_detail/presentation/extension/quick_create_rule_from_collapsed_email_success.dart';
@@ -65,6 +69,8 @@ class ThreadDetailController extends BaseController {
final PrintEmailInteractor _printEmailInteractor;
final GetEmailContentInteractor _getEmailContentInteractor;
final DownloadAttachmentForWebInteractor _downloadAttachmentForWebInteractor;
final MarkAsStarMultipleEmailInteractor markAsStarMultipleEmailInteractor;
final MarkAsMultipleEmailReadInteractor markAsMultipleEmailReadInteractor;
ThreadDetailController(
this._getEmailIdsByThreadIdInteractor,
@@ -74,16 +80,18 @@ class ThreadDetailController extends BaseController {
this._printEmailInteractor,
this._getEmailContentInteractor,
this._downloadAttachmentForWebInteractor,
this.markAsStarMultipleEmailInteractor,
this.markAsMultipleEmailReadInteractor,
);
final emailIdsPresentation = <EmailId, PresentationEmail?>{}.obs;
final currentExpandedEmailId = Rxn<EmailId>();
final currentEmailLoaded = Rxn<EmailLoaded>();
final emailsInThreadDetailInfo = RxList<EmailInThreadDetailInfo>();
late final EmailActionReactor emailActionReactor;
final additionalProperties = Properties({
IndividualHeaderIdentifier.listPostHeader.value,
IndividualHeaderIdentifier.listUnsubscribeHeader.value,
EmailProperty.references,
EmailProperty.messageId,
});
@@ -100,7 +108,6 @@ class ThreadDetailController extends BaseController {
accountId!,
sentMailboxId!,
ownEmailAddress!,
selectedEmailId: mailboxDashBoardController.selectedEmail.value?.id,
));
}
},
@@ -227,8 +234,18 @@ class ThreadDetailController extends BaseController {
handleGetEmailsByIdsSuccess(success);
} else if (success is MarkAsEmailReadSuccess) {
markCollapsedEmailReadSuccess(success);
} else if (success is MarkAsStarEmailSuccess) {
markCollapsedEmailStarSuccess(success);
} else if (success is MarkAsMultipleEmailReadAllSuccess) {
handleMarkMultipleEmailsReadSuccess(
success,
success.readActions,
success.emailIds,
);
} else if (success is MarkAsMultipleEmailReadHasSomeEmailFailure) {
handleMarkMultipleEmailsReadSuccess(
success,
success.readActions,
success.successEmailIds,
);
} else if (success is CreateNewRuleFilterSuccess) {
quickCreateRuleFromCollapsedEmailSuccess(success);
} else if (success is DownloadAttachmentForWebSuccess) {
@@ -261,6 +278,10 @@ class ThreadDetailController extends BaseController {
}
return;
}
if (failure is MarkAsMultipleEmailReadFailure ||
failure is MarkAsStarMultipleEmailFailure) {
toastManager.showMessageFailure(failure as FeatureFailure);
}
super.handleFailureViewState(failure);
}
}
@@ -4,9 +4,10 @@ import 'package:core/utils/platform_info.dart';
import 'package:flutter/widgets.dart';
import 'package:get/get_instance/get_instance.dart';
import 'package:get/state_manager.dart';
import 'package:jmap_dart_client/jmap/core/session/session.dart';
import 'package:jmap_dart_client/jmap/mail/email/email.dart';
import 'package:model/email/presentation_email.dart';
import 'package:tmail_ui_user/features/base/base_controller.dart';
import 'package:tmail_ui_user/features/base/reloadable/reloadable_controller.dart';
import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart';
import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/model/dashboard_routes.dart';
import 'package:tmail_ui_user/features/thread_detail/domain/state/get_thread_detail_status_state.dart';
@@ -16,7 +17,7 @@ import 'package:tmail_ui_user/features/thread_detail/presentation/extension/init
import 'package:tmail_ui_user/features/thread_detail/presentation/extension/refresh_thread_detail_on_setting_changed.dart';
import 'package:tmail_ui_user/features/thread_detail/presentation/model/thread_detail_setting_status.dart';
class ThreadDetailManager extends BaseController {
class ThreadDetailManager extends ReloadableController {
final mailboxDashBoardController = Get.find<MailboxDashBoardController>();
final GetThreadDetailStatusInteractor _getThreadDetailStatusInteractor;
@@ -86,6 +87,12 @@ class ThreadDetailManager extends BaseController {
);
}
@override
void handleReloaded(Session session) {
super.handleReloaded(session);
consumeState(_getThreadDetailStatusInteractor.execute());
}
@override
void handleSuccessViewState(Success success) {
if (success is GetThreadDetailStatusSuccess) {
@@ -1,4 +1,3 @@
import 'package:collection/collection.dart';
import 'package:core/presentation/extensions/color_extension.dart';
import 'package:core/presentation/state/failure.dart';
import 'package:core/presentation/state/success.dart';
@@ -8,19 +7,17 @@ import 'package:dartz/dartz.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:model/extensions/session_extension.dart';
import 'package:model/email/email_action_type.dart';
import 'package:model/mailbox/presentation_mailbox.dart';
import 'package:tmail_ui_user/features/email/presentation/action/email_ui_action.dart';
import 'package:tmail_ui_user/features/email/presentation/styles/email_view_app_bar_widget_styles.dart';
import 'package:tmail_ui_user/features/email/presentation/widgets/email_view_bottom_bar_widget.dart';
import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/extensions/handle_open_context_menu_extension.dart';
import 'package:tmail_ui_user/features/manage_account/presentation/extensions/vacation_response_extension.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/close_thread_detail_action.dart';
import 'package:tmail_ui_user/features/thread_detail/presentation/extension/get_thread_detail_email_mailbox_contains.dart';
import 'package:tmail_ui_user/features/thread_detail/presentation/extension/get_thread_detail_action_status.dart';
import 'package:tmail_ui_user/features/thread_detail/presentation/extension/get_thread_details_email_views.dart';
import 'package:tmail_ui_user/features/thread_detail/presentation/extension/on_thread_detail_action_click.dart';
import 'package:tmail_ui_user/features/thread_detail/presentation/extension/on_thread_page_changed.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/thread_detail_controller.dart';
import 'package:tmail_ui_user/features/thread_detail/presentation/widgets/thread_detail_app_bar.dart';
import 'package:tmail_ui_user/main/localizations/app_localizations.dart';
@@ -35,49 +32,18 @@ class ThreadDetailView extends GetWidget<ThreadDetailController> {
Widget bodyWidget = Column(
children: [
Obx(() {
final isLoading = showLoadingView(controller.viewState.value);
return ThreadDetailAppBar(
responsiveUtils: controller.responsiveUtils,
imagePaths: controller.imagePaths,
isSearchRunning: controller.isSearchRunning,
closeThreadDetailAction: controller.closeThreadDetailAction,
lastEmailOfThread: controller.emailIdsPresentation.values.lastOrNull,
ownUserName: controller.session?.getOwnEmailAddress() ?? '',
isThreadDetailEnabled: controller.isThreadDetailEnabled,
mailboxContain: _getMailboxContain(),
onEmailActionClick: isLoading
? null
: controller.threadDetailOnEmailActionClick,
onMoreActionClick: (presentationEmail, position) => isLoading
? null
: controller.emailActionReactor.handleMoreEmailAction(
mailboxContain: controller.getThreadDetailEmailMailboxContains(
presentationEmail,
),
presentationEmail: presentationEmail,
position: position,
responsiveUtils: controller.responsiveUtils,
imagePaths: controller.imagePaths,
username: controller.session?.username,
handleEmailAction: controller.threadDetailOnEmailActionClick,
additionalActions: [
if (controller.responsiveUtils.isMobile(context)) ...[
EmailActionType.forward,
EmailActionType.replyAll,
EmailActionType.replyToList,
],
EmailActionType.markAsStarred,
EmailActionType.unMarkAsStarred,
EmailActionType.moveToTrash,
EmailActionType.deletePermanently,
EmailActionType.printAll,
EmailActionType.moveToMailbox,
],
emailIsRead: presentationEmail.hasRead,
openBottomSheetContextMenu: controller.mailboxDashBoardController.openBottomSheetContextMenu,
openPopupMenu: controller.mailboxDashBoardController.openPopupMenu,
),
threadActionReady: controller.emailsInThreadDetailInfo.isNotEmpty &&
controller.emailIdsPresentation.length > 1,
threadDetailIsStarred: controller.threadDetailIsStarred,
onThreadActionClick: controller.onThreadDetailActionClick,
onThreadMoreActionClick: controller.onThreadDetailMoreActionClick,
optionWidgets: [
if (controller.previousAvailable)
TMailButtonWidget.fromIcon(
@@ -3,7 +3,10 @@ import 'dart:math';
import 'package:collection/collection.dart';
import 'package:flutter/material.dart';
import 'package:jmap_dart_client/jmap/mail/email/email.dart';
import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart';
import 'package:model/email/presentation_email.dart';
import 'package:tmail_ui_user/features/thread/presentation/extensions/list_presentation_email_extensions.dart';
import 'package:tmail_ui_user/features/thread_detail/domain/model/email_in_thread_detail_info.dart';
class ThreadDetailPresentationUtils {
const ThreadDetailPresentationUtils._();
@@ -90,4 +93,32 @@ class ThreadDetailPresentationUtils {
...created,
];
}
static List<EmailInThreadDetailInfo> refreshThreadDetailInfo({
required List<EmailInThreadDetailInfo> original,
required List<PresentationEmail> created,
required List<PresentationEmail> updated,
required List<EmailId> destroyed,
required MailboxId? sentMailboxId,
required String? ownEmailAddress,
}) {
return [
...original
.whereNot((email) => destroyed.contains(email.emailId))
.map((email) {
final updatedInOriginal = updated.firstWhereOrNull(
(updatedEmail) => updatedEmail.id == email.emailId,
);
return email.copyWith(
keywords: updatedInOriginal?.keywords,
mailboxIds: updatedInOriginal?.mailboxIds,
);
}),
...created.toEmailsInThreadDetailInfo(
sentMailboxId: sentMailboxId,
ownEmailAddress: ownEmailAddress,
),
];
}
}
@@ -4,16 +4,15 @@ import 'package:core/presentation/views/button/tmail_button_widget.dart';
import 'package:core/utils/platform_info.dart';
import 'package:flutter/material.dart';
import 'package:model/email/email_action_type.dart';
import 'package:model/email/presentation_email.dart';
import 'package:model/extensions/presentation_email_extension.dart';
import 'package:model/mailbox/presentation_mailbox.dart';
import 'package:tmail_ui_user/features/email/presentation/styles/email_view_app_bar_widget_styles.dart';
import 'package:tmail_ui_user/features/email/presentation/utils/email_utils.dart';
import 'package:tmail_ui_user/features/email/presentation/widgets/email_view_app_bar_widget.dart';
import 'package:tmail_ui_user/features/email/presentation/widgets/email_view_back_button.dart';
import 'package:tmail_ui_user/main/localizations/app_localizations.dart';
import 'package:tmail_ui_user/main/utils/app_utils.dart';
typedef OnThreadActionClick = void Function(EmailActionType);
typedef OnThreadMoreActionClick = void Function(RelativeRect?);
class ThreadDetailAppBar extends StatelessWidget {
const ThreadDetailAppBar({
super.key,
@@ -21,33 +20,29 @@ class ThreadDetailAppBar extends StatelessWidget {
required this.imagePaths,
required this.isSearchRunning,
required this.closeThreadDetailAction,
required this.lastEmailOfThread,
required this.ownUserName,
required this.threadActionReady,
required this.threadDetailIsStarred,
required this.isThreadDetailEnabled,
this.mailboxContain,
this.optionWidgets = const [],
this.onEmailActionClick,
this.onMoreActionClick,
this.onThreadActionClick,
this.onThreadMoreActionClick,
});
final ResponsiveUtils responsiveUtils;
final ImagePaths imagePaths;
final bool isSearchRunning;
final void Function(BuildContext context) closeThreadDetailAction;
final PresentationEmail? lastEmailOfThread;
final String ownUserName;
final bool threadActionReady;
final bool threadDetailIsStarred;
final bool isThreadDetailEnabled;
final PresentationMailbox? mailboxContain;
final List<Widget> optionWidgets;
final OnEmailActionClick? onEmailActionClick;
final OnMoreActionClick? onMoreActionClick;
final OnThreadActionClick? onThreadActionClick;
final OnThreadMoreActionClick? onThreadMoreActionClick;
@override
Widget build(BuildContext context) {
final isReplyToListEnabled = EmailUtils.isReplyToListEnabled(
lastEmailOfThread?.listPost ?? '',
);
final child = LayoutBuilder(
builder: (context, constraints) {
Widget backButton = EmailViewBackButton(
@@ -86,61 +81,31 @@ class ThreadDetailAppBar extends StatelessWidget {
mainAxisAlignment: MainAxisAlignment.end,
children: [
if (_supportDisplayMailboxNameTitle(context)) backButton,
if (lastEmailOfThread != null && isThreadDetailEnabled) ...[
if (isThreadDetailEnabled && threadActionReady)
_ThreadDetailAppBarButton(
icon: imagePaths.icReply,
tooltipMessage: AppLocalizations.of(context).reply,
icon: threadDetailIsStarred
? imagePaths.icStar
: imagePaths.icUnStar,
tooltipMessage: threadDetailIsStarred
? AppLocalizations.of(context).not_starred
: AppLocalizations.of(context).mark_as_starred,
responsiveUtils: responsiveUtils,
onTapActionCallback: (_) => onEmailActionClick?.call(
lastEmailOfThread!,
EmailActionType.reply,
),
iconColor: null,
onTapActionCallback: threadDetailIsStarred
? (_) => onThreadActionClick?.call(EmailActionType.unMarkAsStarred)
: (_) => onThreadActionClick?.call(EmailActionType.markAsStarred),
),
if (!responsiveUtils.isMobile(context)) ...[
if (lastEmailOfThread!.getCountMailAddressWithoutMe(ownUserName) > 1)
_ThreadDetailAppBarButton(
icon: imagePaths.icReplyAll,
tooltipMessage: AppLocalizations.of(context).reply_all,
responsiveUtils: responsiveUtils,
onTapActionCallback: (_) => onEmailActionClick?.call(
lastEmailOfThread!,
EmailActionType.replyAll,
),
),
if (isReplyToListEnabled)
_ThreadDetailAppBarButton(
icon: imagePaths.icReply,
tooltipMessage: AppLocalizations.of(context).replyToList,
responsiveUtils: responsiveUtils,
onTapActionCallback: (_) => onEmailActionClick?.call(
lastEmailOfThread!,
EmailActionType.replyToList,
),
),
_ThreadDetailAppBarButton(
icon: imagePaths.icForward,
tooltipMessage: AppLocalizations.of(context).forward,
responsiveUtils: responsiveUtils,
onTapActionCallback: (_) => onEmailActionClick?.call(
lastEmailOfThread!,
EmailActionType.forward,
),
),
],
if (!responsiveUtils.isMobile(context)) const Spacer(),
] else const Spacer(),
if (!responsiveUtils.isMobile(context)) const Spacer(),
if (AppUtils.getCurrentDirection(context) == TextDirection.rtl)
...optionWidgets.reversed
else
...optionWidgets,
if (lastEmailOfThread != null)
if (isThreadDetailEnabled && threadActionReady)
_ThreadDetailAppBarButton(
icon: imagePaths.icMoreVertical,
tooltipMessage: AppLocalizations.of(context).more,
responsiveUtils: responsiveUtils,
onTapActionCallback: lastEmailOfThread != null
? (position) => onMoreActionClick?.call(lastEmailOfThread!, position)
: null,
onTapActionCallback: onThreadMoreActionClick,
),
],
),
@@ -175,22 +140,25 @@ class _ThreadDetailAppBarButton extends StatelessWidget {
required this.tooltipMessage,
required this.onTapActionCallback,
required this.responsiveUtils,
this.iconColor = EmailViewAppBarWidgetStyles.iconColor,
});
final String icon;
final String tooltipMessage;
final void Function(RelativeRect? position)? onTapActionCallback;
final ResponsiveUtils responsiveUtils;
final Color? iconColor;
@override
Widget build(BuildContext context) {
final smallScreen = responsiveUtils.isScreenWithShortestSide(context);
return TMailButtonWidget.fromIcon(
icon: icon,
iconSize: EmailViewAppBarWidgetStyles.buttonIconSize,
iconColor: EmailViewAppBarWidgetStyles.iconColor,
iconColor: iconColor,
backgroundColor: Colors.transparent,
tooltipMessage: AppLocalizations.of(context).more,
tooltipMessage: tooltipMessage,
onTapActionCallback:
smallScreen ? () => onTapActionCallback?.call(null) : null,
onTapActionAtPositionCallback: !smallScreen
@@ -23,7 +23,6 @@ class ThreadDetailCollapsedEmail extends StatelessWidget {
this.mailboxContain,
this.emailLoaded,
this.onEmailActionClick,
this.onMoreActionClick,
this.onToggleThreadDetailCollapseExpand,
});
@@ -35,7 +34,6 @@ class ThreadDetailCollapsedEmail extends StatelessWidget {
final PresentationMailbox? mailboxContain;
final EmailLoaded? emailLoaded;
final OnEmailActionClick? onEmailActionClick;
final OnMoreActionClick? onMoreActionClick;
final VoidCallback? onToggleThreadDetailCollapseExpand;
String get preview => presentationEmail.getPartialContent();
@@ -69,7 +67,6 @@ class ThreadDetailCollapsedEmail extends StatelessWidget {
emailLoaded: emailLoaded,
isInsideThreadDetailView: true,
onEmailActionClick: onEmailActionClick,
onMoreActionClick: onMoreActionClick,
openEmailAddressDetailAction: openEmailAddressDetailAction,
showRecipients: false,
onTapAvatarActionClick: onToggleThreadDetailCollapseExpand,
@@ -7,6 +7,7 @@ import 'package:mockito/mockito.dart';
import 'package:model/email/email_in_thread_status.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/model/email_in_thread_detail_info.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';
@@ -62,6 +63,8 @@ void main() {
};
when(threadDetailController.emailIdsPresentation)
.thenReturn(initialEmailIdsPresentation.obs);
when(threadDetailController.emailsInThreadDetailInfo)
.thenReturn(<EmailInThreadDetailInfo>[].obs);
when(threadDetailController.mailboxDashBoardController)
.thenReturn(mailboxDashBoardController);
when(mailboxDashBoardController.selectedEmail)
@@ -11,6 +11,7 @@ import 'package:model/email/presentation_email.dart';
import 'package:tmail_ui_user/features/email/presentation/action/email_ui_action.dart';
import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart';
import 'package:tmail_ui_user/features/thread/data/model/email_change_response.dart';
import 'package:tmail_ui_user/features/thread_detail/domain/model/email_in_thread_detail_info.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/domain/state/get_emails_by_ids_state.dart';
import 'package:tmail_ui_user/features/thread_detail/domain/usecases/get_thread_by_id_interactor.dart';
@@ -98,6 +99,8 @@ void main() {
.thenReturn(MailboxId(Id('sent-mailbox-id')));
when(threadDetailController.ownEmailAddress)
.thenReturn('9jEYK@example.com');
when(threadDetailController.emailsInThreadDetailInfo)
.thenReturn(<EmailInThreadDetailInfo>[].obs);
// act
threadDetailController.handleRefreshThreadDetailAction(
@@ -8,6 +8,7 @@ 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/presentation/extensions/list_presentation_email_extensions.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/state/get_thread_by_id_state.dart';
import 'package:tmail_ui_user/features/thread_detail/domain/usecases/get_thread_by_id_interactor.dart';
@@ -85,7 +86,14 @@ void main() {
expect(
streamsConsumed,
emitsInOrder([
Right(PreloadEmailIdsInThreadSuccess([selectedEmail.id!], threadId: ThreadId(Id('1')))),
Right(PreloadEmailIdsInThreadSuccess(
[selectedEmail.id!],
threadId: ThreadId(Id('1')),
emailsInThreadDetailInfo: [selectedEmail].toEmailsInThreadDetailInfo(
sentMailboxId: null,
ownEmailAddress: null,
),
)),
Right(PreloadEmailsByIdsSuccess([selectedEmail])),
]),
);
@@ -4,6 +4,7 @@ import 'package:jmap_dart_client/jmap/mail/email/email.dart';
import 'package:jmap_dart_client/jmap/mail/email/keyword_identifier.dart';
import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart';
import 'package:model/email/presentation_email.dart';
import 'package:tmail_ui_user/features/thread_detail/domain/model/email_in_thread_detail_info.dart';
import 'package:tmail_ui_user/features/thread_detail/presentation/utils/thread_detail_presentation_utils.dart';
void main() {
@@ -492,5 +493,248 @@ void main() {
expect(result, isEmpty);
});
});
group('refreshThreadDetailInfo', () {
final sentMailboxId = MailboxId(Id('sent'));
const ownEmailAddress = 'user@example';
test(
'should return empty list when all inputs are empty',
() {
// arrange
final original = <EmailInThreadDetailInfo>[];
final created = <PresentationEmail>[];
final updated = <PresentationEmail>[];
final destroyed = <EmailId>[];
// act
final result = ThreadDetailPresentationUtils.refreshThreadDetailInfo(
original: original,
created: created,
updated: updated,
destroyed: destroyed,
sentMailboxId: sentMailboxId,
ownEmailAddress: ownEmailAddress,
);
// assert
expect(result, isEmpty);
},
);
test(
'should return original list when no changes are made',
() {
// arrange
final id = Id('email-1');
final emailId = EmailId(id);
final original = [
EmailInThreadDetailInfo(
emailId: emailId,
keywords: {},
mailboxIds: {},
isValidToDisplay: true,
),
];
final created = <PresentationEmail>[];
final updated = <PresentationEmail>[];
final destroyed = <EmailId>[];
// act
final result = ThreadDetailPresentationUtils.refreshThreadDetailInfo(
original: original,
created: created,
updated: updated,
destroyed: destroyed,
sentMailboxId: sentMailboxId,
ownEmailAddress: ownEmailAddress,
);
// assert
expect(result, equals(original));
},
);
test(
'should return created emails when no original emails are present',
() {
// arrange
final id = Id('email-1');
final emailId = EmailId(id);
final created = [
PresentationEmail(
id: emailId,
keywords: {},
mailboxIds: {},
),
];
final original = <EmailInThreadDetailInfo>[];
final updated = <PresentationEmail>[];
final destroyed = <EmailId>[];
// act
final result = ThreadDetailPresentationUtils.refreshThreadDetailInfo(
original: original,
created: created,
updated: updated,
destroyed: destroyed,
sentMailboxId: sentMailboxId,
ownEmailAddress: ownEmailAddress,
);
// assert
expect(result, hasLength(1));
expect(result.first.emailId, equals(emailId));
expect(result.first.keywords, equals({}));
expect(result.first.mailboxIds, equals({}));
},
);
test(
'should return updated emails when emails are updated',
() {
// arrange
final id = Id('email-1');
final emailId = EmailId(id);
final original = [
EmailInThreadDetailInfo(
emailId: emailId,
keywords: {},
mailboxIds: {},
isValidToDisplay: true,
),
];
final updated = [
PresentationEmail(
id: emailId,
keywords: {KeyWordIdentifier('test'): true},
mailboxIds: {},
),
];
final created = <PresentationEmail>[];
final destroyed = <EmailId>[];
// act
final result = ThreadDetailPresentationUtils.refreshThreadDetailInfo(
original: original,
created: created,
updated: updated,
destroyed: destroyed,
sentMailboxId: sentMailboxId,
ownEmailAddress: ownEmailAddress,
);
// assert
expect(result, hasLength(1));
expect(result.first.emailId, equals(emailId));
expect(result.first.keywords, equals({KeyWordIdentifier('test'): true}));
expect(result.first.mailboxIds, equals({}));
},
);
test(
'should remove destroyed emails from the result',
() {
// arrange
final id1 = Id('email-1');
final emailId1 = EmailId(id1);
final id2 = Id('email-2');
final emailId2 = EmailId(id2);
final original = [
EmailInThreadDetailInfo(
emailId: emailId1,
keywords: {},
mailboxIds: {},
isValidToDisplay: true,
),
EmailInThreadDetailInfo(
emailId: emailId2,
keywords: {},
mailboxIds: {},
isValidToDisplay: true,
),
];
final created = <PresentationEmail>[];
final updated = <PresentationEmail>[];
final destroyed = [emailId1];
// act
final result = ThreadDetailPresentationUtils.refreshThreadDetailInfo(
original: original,
created: created,
updated: updated,
destroyed: destroyed,
sentMailboxId: sentMailboxId,
ownEmailAddress: ownEmailAddress,
);
// assert
expect(result, hasLength(1));
expect(result.first.emailId, equals(emailId2));
expect(result.first.keywords, equals({}));
expect(result.first.mailboxIds, equals({}));
},
);
test(
'should apply all changes when multiple changes are present',
() {
// arrange
final id1 = Id('email-1');
final emailId1 = EmailId(id1);
final id2 = Id('email-2');
final emailId2 = EmailId(id2);
final id3 = Id('email-3');
final emailId3 = EmailId(id3);
final original = [
EmailInThreadDetailInfo(
emailId: emailId1,
keywords: {},
mailboxIds: {},
isValidToDisplay: true,
),
EmailInThreadDetailInfo(
emailId: emailId2,
keywords: {},
mailboxIds: {},
isValidToDisplay: true,
),
];
final created = [
PresentationEmail(
id: emailId3,
keywords: {KeyWordIdentifier('test'): true},
mailboxIds: {},
),
];
final updated = [
PresentationEmail(
id: emailId1,
keywords: {KeyWordIdentifier('updated'): true},
mailboxIds: {},
),
];
final destroyed = [emailId2];
// act
final result = ThreadDetailPresentationUtils.refreshThreadDetailInfo(
original: original,
created: created,
updated: updated,
destroyed: destroyed,
sentMailboxId: sentMailboxId,
ownEmailAddress: ownEmailAddress,
);
// assert
expect(result, hasLength(2));
expect(result.first.emailId, equals(emailId1));
expect(result.first.keywords, equals({KeyWordIdentifier('updated'): true}));
expect(result.first.mailboxIds, equals({}));
expect(result.last.emailId, equals(emailId3));
expect(result.last.keywords, equals({KeyWordIdentifier('test'): true}));
expect(result.last.mailboxIds, equals({}));
},
);
});
});
}