diff --git a/docs/adr/0068-thread-level-actions.md b/docs/adr/0068-thread-level-actions.md new file mode 100644 index 000000000..fafb3beb7 --- /dev/null +++ b/docs/adr/0068-thread-level-actions.md @@ -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. diff --git a/lib/features/composer/presentation/extensions/email_action_type_extension.dart b/lib/features/composer/presentation/extensions/email_action_type_extension.dart index fb84d708c..5117ad5c3 100644 --- a/lib/features/composer/presentation/extensions/email_action_type_extension.dart +++ b/lib/features/composer/presentation/extensions/email_action_type_extension.dart @@ -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; diff --git a/lib/features/email/presentation/controller/single_email_controller.dart b/lib/features/email/presentation/controller/single_email_controller.dart index ec0130c1e..8e223ad3d 100644 --- a/lib/features/email/presentation/controller/single_email_controller.dart +++ b/lib/features/email/presentation/controller/single_email_controller.dart @@ -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); diff --git a/lib/features/email/presentation/email_view.dart b/lib/features/email/presentation/email_view.dart index c659b6a93..90d28cb66 100644 --- a/lib/features/email/presentation/email_view.dart +++ b/lib/features/email/presentation/email_view.dart @@ -252,40 +252,37 @@ class EmailView extends GetWidget { 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), diff --git a/lib/features/email/presentation/extensions/email_extension.dart b/lib/features/email/presentation/extensions/email_extension.dart index aef423c14..123e24c5a 100644 --- a/lib/features/email/presentation/extensions/email_extension.dart +++ b/lib/features/email/presentation/extensions/email_extension.dart @@ -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; } diff --git a/lib/features/email/presentation/widgets/email_view_app_bar_widget.dart b/lib/features/email/presentation/widgets/email_view_app_bar_widget.dart index 1233f9320..35e622c0f 100644 --- a/lib/features/email/presentation/widgets/email_view_app_bar_widget.dart +++ b/lib/features/email/presentation/widgets/email_view_app_bar_widget.dart @@ -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), ]; } } \ No newline at end of file diff --git a/lib/features/email/presentation/widgets/information_sender_and_receiver_builder.dart b/lib/features/email/presentation/widgets/information_sender_and_receiver_builder.dart index 6ee5ce227..c752145a1 100644 --- a/lib/features/email/presentation/widgets/information_sender_and_receiver_builder.dart +++ b/lib/features/email/presentation/widgets/information_sender_and_receiver_builder.dart @@ -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, ), ), ), diff --git a/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart b/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart index add07afc0..cc5e46300 100644 --- a/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart +++ b/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart @@ -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 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 = >{}; + 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, diff --git a/lib/features/thread/data/extensions/list_email_extension.dart b/lib/features/thread/data/extensions/list_email_extension.dart index 9d4a22b5e..05befc913 100644 --- a/lib/features/thread/data/extensions/list_email_extension.dart +++ b/lib/features/thread/data/extensions/list_email_extension.dart @@ -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 { Map toMapCache(AccountId accountId, UserName userName) { @@ -39,4 +42,23 @@ extension ListEmailExtension on List { return this; } + + List 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(); + } } \ No newline at end of file diff --git a/lib/features/thread/presentation/extensions/list_presentation_email_extensions.dart b/lib/features/thread/presentation/extensions/list_presentation_email_extensions.dart index 600cecd74..664490898 100644 --- a/lib/features/thread/presentation/extensions/list_presentation_email_extensions.dart +++ b/lib/features/thread/presentation/extensions/list_presentation_email_extensions.dart @@ -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 { return null; } } + + List 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(); + } } \ No newline at end of file diff --git a/lib/features/thread_detail/data/repository/thread_detail_repository_impl.dart b/lib/features/thread_detail/data/repository/thread_detail_repository_impl.dart index 1ec6957f0..3471abfb6 100644 --- a/lib/features/thread_detail/data/repository/thread_detail_repository_impl.dart +++ b/lib/features/thread_detail/data/repository/thread_detail_repository_impl.dart @@ -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 threadDetailDataSource; @override - Future> getThreadById( + Future> 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> _filterBadEmails( + Future> _filterBadEmails( Session session, AccountId accountId, List 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 @@ -130,4 +113,28 @@ class ThreadDetailRepositoryImpl implements ThreadDetailRepository { return threadDetailDataSource[DataSourceType.local]! .getThreadDetailStatus(); } +} + +extension SortListEmailInThreadDetailInfo on Map { + Map sortWithResult( + EmailComparator comparator, + ) { + final originalEmails = keys.toList(); + final sortedEmails = originalEmails.sortWithResult(comparator); + return { + for (final email in sortedEmails) + email: this[email] ?? false, + }; + } + + List toEmailsInThreadDetailInfo() { + return entries.map( + (entry) => EmailInThreadDetailInfo( + emailId: entry.key.id!, + keywords: entry.key.keywords, + mailboxIds: entry.key.mailboxIds, + isValidToDisplay: entry.value, + ), + ).toList(); + } } \ No newline at end of file diff --git a/lib/features/thread_detail/domain/extensions/list_email_in_thread_detail_info_extension.dart b/lib/features/thread_detail/domain/extensions/list_email_in_thread_detail_info_extension.dart new file mode 100644 index 000000000..fa1453447 --- /dev/null +++ b/lib/features/thread_detail/domain/extensions/list_email_in_thread_detail_info_extension.dart @@ -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 { + List get emailIdsToDisplay => + where((email) => email.isValidToDisplay) + .map((email) => email.emailId) + .toList(); +} \ No newline at end of file diff --git a/lib/features/thread_detail/domain/model/email_in_thread_detail_info.dart b/lib/features/thread_detail/domain/model/email_in_thread_detail_info.dart new file mode 100644 index 000000000..e9282e89a --- /dev/null +++ b/lib/features/thread_detail/domain/model/email_in_thread_detail_info.dart @@ -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? keywords; + final Map? mailboxIds; + final bool isValidToDisplay; + + @override + List get props => [emailId, keywords, mailboxIds, isValidToDisplay]; + + EmailInThreadDetailInfo copyWith({ + EmailId? emailId, + Map? keywords, + Map? 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; + } +} \ No newline at end of file diff --git a/lib/features/thread_detail/domain/repository/thread_detail_repository.dart b/lib/features/thread_detail/domain/repository/thread_detail_repository.dart index 33bc4bd6a..61f17e211 100644 --- a/lib/features/thread_detail/domain/repository/thread_detail_repository.dart +++ b/lib/features/thread_detail/domain/repository/thread_detail_repository.dart @@ -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> getThreadById( + Future> getThreadById( ThreadId threadId, Session session, AccountId accountId, MailboxId sentMailboxId, - String ownEmailAddress, { - required EmailId? selectedEmailId, - }); + String ownEmailAddress, + ); Future> getEmailsByIds( Session session, diff --git a/lib/features/thread_detail/domain/state/get_thread_by_id_state.dart b/lib/features/thread_detail/domain/state/get_thread_by_id_state.dart index 553999919..a583c4a6f 100644 --- a/lib/features/thread_detail/domain/state/get_thread_by_id_state.dart +++ b/lib/features/thread_detail/domain/state/get_thread_by_id_state.dart @@ -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 emailsInThreadDetailInfo; GetThreadByIdSuccess( this.emailIds, { required this.threadId, this.updateCurrentThreadDetail = false, this.skipLoadingMetadata = false, + this.emailsInThreadDetailInfo = const [], }); @override List 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 { diff --git a/lib/features/thread_detail/domain/usecases/get_thread_by_id_interactor.dart b/lib/features/thread_detail/domain/usecases/get_thread_by_id_interactor.dart index 57ed3a78c..93413fe82 100644 --- a/lib/features/thread_detail/domain/usecases/get_thread_by_id_interactor.dart +++ b/lib/features/thread_detail/domain/usecases/get_thread_by_id_interactor.dart @@ -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, )); diff --git a/lib/features/thread_detail/presentation/extension/get_thread_detail_action_status.dart b/lib/features/thread_detail/presentation/extension/get_thread_detail_action_status.dart new file mode 100644 index 000000000..70ac3643b --- /dev/null +++ b/lib/features/thread_detail/presentation/extension/get_thread_detail_action_status.dart @@ -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; + }, + ); + } +} diff --git a/lib/features/thread_detail/presentation/extension/get_thread_details_email_views.dart b/lib/features/thread_detail/presentation/extension/get_thread_details_email_views.dart index 903cd8c33..c32c3f69b 100644 --- a/lib/features/thread_detail/presentation/extension/get_thread_details_email_views.dart +++ b/lib/features/thread_detail/presentation/extension/get_thread_details_email_views.dart @@ -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 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); }, diff --git a/lib/features/thread_detail/presentation/extension/handle_get_email_ids_by_thread_id_success.dart b/lib/features/thread_detail/presentation/extension/handle_get_email_ids_by_thread_id_success.dart index a59651caf..cdfe4be86 100644 --- a/lib/features/thread_detail/presentation/extension/handle_get_email_ids_by_thread_id_success.dart +++ b/lib/features/thread_detail/presentation/extension/handle_get_email_ids_by_thread_id_success.dart @@ -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; + } } } \ No newline at end of file diff --git a/lib/features/thread_detail/presentation/extension/handle_mark_multiple_emails_read_success.dart b/lib/features/thread_detail/presentation/extension/handle_mark_multiple_emails_read_success.dart new file mode 100644 index 000000000..8ad63a528 --- /dev/null +++ b/lib/features/thread_detail/presentation/extension/handle_mark_multiple_emails_read_success.dart @@ -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 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); + } + } +} \ No newline at end of file diff --git a/lib/features/thread_detail/presentation/extension/handle_refresh_thread_detail_action.dart b/lib/features/thread_detail/presentation/extension/handle_refresh_thread_detail_action.dart index f9b310a58..924e7a176 100644 --- a/lib/features/thread_detail/presentation/extension/handle_refresh_thread_detail_action.dart +++ b/lib/features/thread_detail/presentation/extension/handle_refresh_thread_detail_action.dart @@ -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, diff --git a/lib/features/thread_detail/presentation/extension/on_thread_detail_action_click.dart b/lib/features/thread_detail/presentation/extension/on_thread_detail_action_click.dart new file mode 100644 index 000000000..2e9bb85c9 --- /dev/null +++ b/lib/features/thread_detail/presentation/extension/on_thread_detail_action_click.dart @@ -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, + ); + } +} \ No newline at end of file diff --git a/lib/features/thread_detail/presentation/extension/thread_detail_on_email_action_click.dart b/lib/features/thread_detail/presentation/extension/thread_detail_on_email_action_click.dart index e7a27e106..6a7419b33 100644 --- a/lib/features/thread_detail/presentation/extension/thread_detail_on_email_action_click.dart +++ b/lib/features/thread_detail/presentation/extension/thread_detail_on_email_action_click.dart @@ -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 _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, - ); - } } \ No newline at end of file diff --git a/lib/features/thread_detail/presentation/extension/thread_detail_on_selected_email_updated.dart b/lib/features/thread_detail/presentation/extension/thread_detail_on_selected_email_updated.dart index b541ef167..5871495d3 100644 --- a/lib/features/thread_detail/presentation/extension/thread_detail_on_selected_email_updated.dart +++ b/lib/features/thread_detail/presentation/extension/thread_detail_on_selected_email_updated.dart @@ -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])), ])); diff --git a/lib/features/thread_detail/presentation/thread_detail_bindings.dart b/lib/features/thread_detail/presentation/thread_detail_bindings.dart index 688fcfc83..06474cddf 100644 --- a/lib/features/thread_detail/presentation/thread_detail_bindings.dart +++ b/lib/features/thread_detail/presentation/thread_detail_bindings.dart @@ -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(), Get.find(), Get.find(), + Get.find(), + Get.find(), )); } diff --git a/lib/features/thread_detail/presentation/thread_detail_controller.dart b/lib/features/thread_detail/presentation/thread_detail_controller.dart index 20f534af8..70e397c70 100644 --- a/lib/features/thread_detail/presentation/thread_detail_controller.dart +++ b/lib/features/thread_detail/presentation/thread_detail_controller.dart @@ -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 = {}.obs; final currentExpandedEmailId = Rxn(); final currentEmailLoaded = Rxn(); + final emailsInThreadDetailInfo = RxList(); 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); } } \ No newline at end of file diff --git a/lib/features/thread_detail/presentation/thread_detail_manager.dart b/lib/features/thread_detail/presentation/thread_detail_manager.dart index 99221f533..a600a6a25 100644 --- a/lib/features/thread_detail/presentation/thread_detail_manager.dart +++ b/lib/features/thread_detail/presentation/thread_detail_manager.dart @@ -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(); 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) { diff --git a/lib/features/thread_detail/presentation/thread_detail_view.dart b/lib/features/thread_detail/presentation/thread_detail_view.dart index 5c84b7ca0..531242c60 100644 --- a/lib/features/thread_detail/presentation/thread_detail_view.dart +++ b/lib/features/thread_detail/presentation/thread_detail_view.dart @@ -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 { 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( diff --git a/lib/features/thread_detail/presentation/utils/thread_detail_presentation_utils.dart b/lib/features/thread_detail/presentation/utils/thread_detail_presentation_utils.dart index 59000fd2f..6017062cb 100644 --- a/lib/features/thread_detail/presentation/utils/thread_detail_presentation_utils.dart +++ b/lib/features/thread_detail/presentation/utils/thread_detail_presentation_utils.dart @@ -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 refreshThreadDetailInfo({ + required List original, + required List created, + required List updated, + required List 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, + ), + ]; + } } \ No newline at end of file diff --git a/lib/features/thread_detail/presentation/widgets/thread_detail_app_bar.dart b/lib/features/thread_detail/presentation/widgets/thread_detail_app_bar.dart index 127174338..48f51b266 100644 --- a/lib/features/thread_detail/presentation/widgets/thread_detail_app_bar.dart +++ b/lib/features/thread_detail/presentation/widgets/thread_detail_app_bar.dart @@ -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 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 @@ -198,4 +166,4 @@ class _ThreadDetailAppBarButton extends StatelessWidget { : null, ); } -} \ No newline at end of file +} diff --git a/lib/features/thread_detail/presentation/widgets/thread_detail_collapsed_email.dart b/lib/features/thread_detail/presentation/widgets/thread_detail_collapsed_email.dart index 6ad5e94e6..6412709bc 100644 --- a/lib/features/thread_detail/presentation/widgets/thread_detail_collapsed_email.dart +++ b/lib/features/thread_detail/presentation/widgets/thread_detail_collapsed_email.dart @@ -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, diff --git a/test/features/thread_detail/presentation/extension/handle_get_email_ids_by_thread_id_success_test.dart b/test/features/thread_detail/presentation/extension/handle_get_email_ids_by_thread_id_success_test.dart index 838f71c7b..835e06920 100644 --- a/test/features/thread_detail/presentation/extension/handle_get_email_ids_by_thread_id_success_test.dart +++ b/test/features/thread_detail/presentation/extension/handle_get_email_ids_by_thread_id_success_test.dart @@ -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([].obs); when(threadDetailController.mailboxDashBoardController) .thenReturn(mailboxDashBoardController); when(mailboxDashBoardController.selectedEmail) diff --git a/test/features/thread_detail/presentation/extension/handle_refresh_thread_detail_action_test.dart b/test/features/thread_detail/presentation/extension/handle_refresh_thread_detail_action_test.dart index 44b779d98..f78f52aa1 100644 --- a/test/features/thread_detail/presentation/extension/handle_refresh_thread_detail_action_test.dart +++ b/test/features/thread_detail/presentation/extension/handle_refresh_thread_detail_action_test.dart @@ -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([].obs); // act threadDetailController.handleRefreshThreadDetailAction( diff --git a/test/features/thread_detail/presentation/extension/thread_detail_on_selected_email_updated_test.dart b/test/features/thread_detail/presentation/extension/thread_detail_on_selected_email_updated_test.dart index be086a2f8..29acc2f66 100644 --- a/test/features/thread_detail/presentation/extension/thread_detail_on_selected_email_updated_test.dart +++ b/test/features/thread_detail/presentation/extension/thread_detail_on_selected_email_updated_test.dart @@ -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])), ]), ); diff --git a/test/features/thread_detail/presentation/utils/thread_detail_presentation_utils_test.dart b/test/features/thread_detail/presentation/utils/thread_detail_presentation_utils_test.dart index 0d9b8f234..a61e741d2 100644 --- a/test/features/thread_detail/presentation/utils/thread_detail_presentation_utils_test.dart +++ b/test/features/thread_detail/presentation/utils/thread_detail_presentation_utils_test.dart @@ -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 = []; + final created = []; + final updated = []; + final destroyed = []; + + // 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 = []; + final updated = []; + final destroyed = []; + + // 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 = []; + final updated = []; + final destroyed = []; + + // 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 = []; + final destroyed = []; + + // 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 = []; + final updated = []; + 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({})); + }, + ); + }); }); }