TF-3715 Thread Detail create resuable actions

TF-3715 Thread Detail refactor actions for expanded email view

TF-3715 Thread Detail apply actions for collapsed email view

TF-3715 Thread Detail fix unit tests

TF-3715 Thread Detail add await for _getEmailContent in composer

TF-3715 Thread Detail remove unused emailLoaded in ThreadDetailAppBar

TF-3715 Thread Detail hide print action when isMobile

TF-3715 Thread Detail allow mark as read in EmailActionReactor

TF-3715 Thread Detail fix loadMoreThreadDetailEmails unit test

TF-3715 Thread Detail fix action small tap region

TF-3715 Thread Detail fix cannot permanent delete email

TF-3715 Thread Detail prevent close when move email

TF-3715 Thread Detail fix refresh when mark star selected collapsed email

TF-3715 Thread Detail update print action display logic

TF-3715 Thread Detail fix duplicate mark star in app bar

TF-3715 Thread Detail create new transformer for reply forward empty content email

TF-3715 Thread Detail fix email not load after open multiple times

TF-3715 Thread Detail fix close when move expanded email

TF-3715 Thread Detail fix duplicate and missing actions

TF-3715 Thread Detail restore editAsNewEmail action to _getEmailContent in setupEmailContent

TF-3715 Thread Detail update print action name

TF-3715 Thread Detail handle move and delete email with 1 item in thread detail

TF-3715 Thread Detail fix tap outside to close context menu

TF-3715 Thread Detail fix create rule and compose from recipient info

TF-3715 Thread Detail handle move and delete email with 1 item in thread detail

TF-3715 Thread Detail Fix cannot create email rule

TF-3715 Thread Detail Fix crash composer

TF-3715 Thread Detail Fix cannot unsubscribe

TF-3715 Thread Detail Fix duplicate actions on single email

TF-3715 Thread Detail Fix refresh view when reply/forward

TF-3715 Thread Detail Revert old logic to _getEmailContent

TF-3715 Thread Detail Update specific cases for UpdatedEmailKeywordsAction

TF-3715 Thread Detail Fix concurrent modification during iteration

TF-3715 Thread Detail Fix unsubscribe for all thread detail emails

TF-3715 Thread Detail Remove unnecessary Future in reactor

TF-3715 Thread Detail Remove dependency from SingleEmailController

TF-3715 Thread Detail Apply individual list post and unsubscribe header

TF-3715 Thread Detail Fix unsubscribe collapsed email

TF-3715 Thread Detail Conditionally inject rule filter interactor
This commit is contained in:
DatDang
2025-05-16 15:37:07 +07:00
committed by Dat H. Pham
parent 421fa321ba
commit 8a2ea62987
38 changed files with 2135 additions and 715 deletions
@@ -0,0 +1,23 @@
import 'package:jmap_dart_client/jmap/mail/email/email.dart';
import 'package:jmap_dart_client/jmap/mail/email/keyword_identifier.dart';
import 'package:tmail_ui_user/features/base/action/ui_action.dart';
class ThreadDetailUIAction extends UIAction {
@override
List<Object?> get props => [];
}
class UpdatedEmailKeywordsAction extends ThreadDetailUIAction {
UpdatedEmailKeywordsAction(
this.emailId,
this.updatedKeyword,
this.value,
);
final EmailId emailId;
final KeyWordIdentifier updatedKeyword;
final bool value;
@override
List<Object?> get props => [emailId, updatedKeyword, value];
}
@@ -10,10 +10,11 @@ extension CloseThreadDetailAction on ThreadDetailController {
} else {
mailboxDashBoardController.dispatchRoute(DashboardRoutes.thread);
}
for (var emailId in emailIdsPresentation.keys) {
mailboxDashBoardController.dispatchEmailUIAction(
CloseEmailInThreadDetailAction(emailId),
);
final cleanUpEmailIds = List.from(emailIdsPresentation.keys);
for (var emailId in cleanUpEmailIds) {
mailboxDashBoardController
..dispatchEmailUIAction(CloseEmailInThreadDetailAction(emailId))
..dispatchEmailUIAction(EmailUIAction());
}
reset();
@@ -1,12 +1,18 @@
import 'package:collection/collection.dart';
import 'package:flutter/material.dart';
import 'package:model/email/email_action_type.dart';
import 'package:model/extensions/presentation_email_extension.dart';
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/get_mailbox_contain_extension.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';
import 'package:tmail_ui_user/features/thread_detail/presentation/extension/toggle_thread_detail_collape_expand.dart';
import 'package:tmail_ui_user/features/thread_detail/presentation/thread_detail_controller.dart';
import 'package:tmail_ui_user/features/thread_detail/presentation/widgets/thread_detail_collapsed_email.dart';
import 'package:tmail_ui_user/features/thread_detail/presentation/widgets/thread_detail_load_more_circle.dart';
import 'package:tmail_ui_user/main/routes/route_navigation.dart';
extension GetThreadDetailEmailViews on ThreadDetailController {
List<Widget> getThreadDetailEmailViews() {
@@ -58,10 +64,37 @@ extension GetThreadDetailEmailViews on ThreadDetailController {
showSubject: isFirstEmailInThreadDetail,
imagePaths: imagePaths,
responsiveUtils: responsiveUtils,
mailboxContain: presentationEmail.mailboxContain,
mailboxContain: presentationEmail.findMailboxContain(
mailboxDashBoardController.mapMailboxById,
),
emailLoaded: null,
openEmailAddressDetailAction: (context, emailAddress) {
// TODO: Next PR
onEmailActionClick: threadDetailOnEmailActionClick,
onMoreActionClick: (presentationEmail, position) => emailActionReactor.handleMoreEmailAction(
mailboxContain: mailboxDashBoardController.getMailboxContain(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,
],
],
),
openEmailAddressDetailAction: (_, emailAddress) {
openEmailAddressDetailAction(emailAddress);
},
onToggleThreadDetailCollapseExpand: () {
toggleThreadDetailCollapeExpand(presentationEmail);
@@ -0,0 +1,74 @@
import 'package:core/presentation/extensions/color_extension.dart';
import 'package:core/presentation/state/failure.dart';
import 'package:core/presentation/state/success.dart';
import 'package:dartz/dartz.dart';
import 'package:tmail_ui_user/features/email/domain/state/download_attachment_for_web_state.dart';
import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/model/download/download_task_state.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 HandleCollapsedEmailDownloadStates on ThreadDetailController {
void handleDownloadProgressState(Either<Failure, Success> state) {
state.fold(
(failure) => null,
(success) {
if (success is StartDownloadAttachmentForWeb &&
!success.previewerSupported
) {
mailboxDashBoardController.addDownloadTask(
DownloadTaskState(
taskId: success.taskId,
attachment: success.attachment,
onCancel: () => success.cancelToken?.cancel(),
),
);
if (currentOverlayContext != null &&
currentContext != null &&
!success.previewerSupported
) {
appToast.showToastMessage(
currentOverlayContext!,
AppLocalizations.of(currentContext!).your_download_has_started,
leadingSVGIconColor: AppColor.primaryColor,
leadingSVGIcon: imagePaths.icDownload,
);
}
} else if (success is DownloadingAttachmentForWeb) {
mailboxDashBoardController.updateDownloadTask(
success.taskId,
(currentTask) {
final newTask = currentTask.copyWith(
progress: success.progress,
downloaded: success.downloaded,
total: success.total,
);
return newTask;
},
);
}
}
);
}
void handleDownloadSuccess(DownloadAttachmentForWebSuccess success) {
mailboxDashBoardController.deleteDownloadTask(success.taskId);
downloadManager.createAnchorElementDownloadFileWeb(
success.bytes,
success.attachment.generateFileName());
}
void handleDownloadFailure(DownloadAttachmentForWebFailure failure) {
if (failure.taskId != null) {
mailboxDashBoardController.deleteDownloadTask(failure.taskId!);
}
if (currentOverlayContext == null || currentContext == null) return;
final message = AppLocalizations.of(currentContext!).downloadMessageAsEMLFailed;
appToast.showToastErrorMessage(currentOverlayContext!, message);
}
}
@@ -25,7 +25,7 @@ extension InitializeThreadDetailEmails on ThreadDetailController {
properties: EmailUtils.getPropertiesForEmailGetMethod(
session!,
accountId!,
),
).union(additionalProperties),
));
}
}
@@ -26,7 +26,7 @@ extension LoadMoreThreadDetailEmails on ThreadDetailController {
properties: EmailUtils.getPropertiesForEmailGetMethod(
session!,
accountId!,
),
).union(additionalProperties),
));
}
}
@@ -0,0 +1,24 @@
import 'package:jmap_dart_client/jmap/mail/email/keyword_identifier.dart';
import 'package:model/email/mark_star_action.dart';
import 'package:model/extensions/presentation_email_extension.dart';
import 'package:tmail_ui_user/features/email/domain/state/mark_as_email_star_state.dart';
import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/extensions/update_current_emails_flags_extension.dart';
import 'package:tmail_ui_user/features/thread_detail/presentation/thread_detail_controller.dart';
extension MarkCollapsedEmailStarSuccess on ThreadDetailController {
void markCollapsedEmailStarSuccess(MarkAsStarEmailSuccess success) {
final updatedMarkedEmail = emailIdsPresentation[success.emailId]
?.updateKeywords({
KeyWordIdentifier.emailFlagged: success.markStarAction == MarkStarAction.markStar,
});
if (updatedMarkedEmail == null) return;
if (mailboxDashBoardController.selectedEmail.value?.id == success.emailId) {
mailboxDashBoardController.updateEmailFlagByEmailIds(
[success.emailId],
markStarAction: success.markStarAction,
);
}
emailIdsPresentation[success.emailId] = updatedMarkedEmail;
}
}
@@ -0,0 +1,18 @@
import 'package:model/email/read_actions.dart';
import 'package:tmail_ui_user/features/email/domain/state/mark_as_email_read_state.dart';
import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/extensions/update_current_emails_flags_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 MarkCollapsedEmailReadSuccess on ThreadDetailController {
void markCollapsedEmailReadSuccess(MarkAsEmailReadSuccess success) {
mailboxDashBoardController.updateEmailFlagByEmailIds(
[success.emailId],
readAction: success.readActions,
);
if (success.readActions == ReadActions.markAsUnread) {
closeThreadDetailAction(currentContext);
}
}
}
@@ -0,0 +1,19 @@
import 'package:tmail_ui_user/features/manage_account/domain/state/create_new_rule_filter_state.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 QuickCreateRuleFromCollapsedEmailSuccess on ThreadDetailController {
void quickCreateRuleFromCollapsedEmailSuccess(
CreateNewRuleFilterSuccess success,
) {
if (success.newListRules.isEmpty ||
currentContext == null ||
currentOverlayContext == null) return;
appToast.showToastSuccessMessage(
currentOverlayContext!,
AppLocalizations.of(currentContext!).newFilterWasCreated,
);
}
}
@@ -0,0 +1,376 @@
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/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/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 ThreadDetailOnEmailActionClick on ThreadDetailController {
void threadDetailOnEmailActionClick(
PresentationEmail presentationEmail,
EmailActionType emailActionType,
) {
switch(emailActionType) {
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;
}
}
void _markRead(PresentationEmail presentationEmail) {
if (session == null || accountId == null) return;
consumeState(emailActionReactor.markAsEmailRead(
session!,
accountId!,
presentationEmail,
readAction: ReadActions.markAsRead,
));
}
void _unRead(PresentationEmail presentationEmail) {
if (session == null || accountId == null) return;
consumeState(emailActionReactor.markAsEmailRead(
session!,
accountId!,
presentationEmail,
readAction: ReadActions.markAsUnread,
));
}
void _markStar(
PresentationEmail presentationEmail,
MarkStarAction markStarAction,
) {
if (session == null || accountId == null) 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: mailboxDashBoardController.selectedMailbox.value,
isSearchEmailRunning: mailboxDashBoardController.searchController.isSearchEmailRunning,
);
if (moveActionRequest == null) return;
mailboxDashBoardController.moveToMailbox(
session!,
accountId!,
moveActionRequest.moveRequest,
moveActionRequest.emailIdsWithReadStatus,
);
}
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,
);
}
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,
);
}
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,
);
}
void _deleteEmailPermanently(PresentationEmail email) {
emailActionReactor.deleteEmailPermanently(
email,
onDeleteEmailRequest: (email) {
popBack();
mailboxDashBoardController.deleteEmailPermanently(email);
},
responsiveUtils: responsiveUtils,
imagePaths: imagePaths,
);
}
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(),
onGetEmailContentFailure: _showUnknownErrorToast,
));
}
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,
onReplyEmailRequest: (presentationEmail, emailLoaded) {
mailboxDashBoardController.openComposer(
ComposerArguments.replyEmail(
presentationEmail: presentationEmail,
content: emailLoaded?.htmlContent,
inlineImages: emailLoaded?.inlineImages,
mailboxRole: presentationEmail.mailboxContain?.role,
messageId: emailLoaded?.emailCurrent?.messageId,
references: emailLoaded?.emailCurrent?.references,
listPost: emailLoaded?.emailCurrent?.listPost,
)
);
},
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: emailLoaded?.emailCurrent?.messageId,
references: emailLoaded?.emailCurrent?.references,
listPost: emailLoaded?.emailCurrent?.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: emailLoaded?.emailCurrent?.messageId,
references: emailLoaded?.emailCurrent?.references,
listPost: emailLoaded?.emailCurrent?.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: emailLoaded?.emailCurrent?.messageId,
references: emailLoaded?.emailCurrent?.references,
)
);
},
emailLoaded: null,
);
}
_showUnknownErrorToast() {
if (currentOverlayContext != null && currentContext != null) {
appToast.showToastErrorMessage(
currentOverlayContext!,
AppLocalizations.of(currentContext!).unknownError,
);
}
}
}
@@ -0,0 +1,29 @@
import 'package:jmap_dart_client/jmap/mail/email/email_address.dart';
import 'package:tmail_ui_user/features/email/presentation/model/composer_arguments.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/thread_detail_controller.dart';
import 'package:tmail_ui_user/main/routes/route_navigation.dart';
extension ThreadDetailOpenEmailAddressDetailAction on ThreadDetailController {
void openEmailAddressDetailAction(EmailAddress emailAddress) {
if (session == null || accountId == null) return;
emailActionReactor.openEmailAddressDialog(
session!,
accountId!,
emailAddress: emailAddress,
responsiveUtils: responsiveUtils,
imagePaths: imagePaths,
appToast: appToast,
onComposeEmailFromEmailAddressRequest: (emailAddress) {
popBack();
mailboxDashBoardController.openComposer(
ComposerArguments.fromEmailAddress(emailAddress),
);
},
onQuickCreateRuleRequest: (quickCreateRuleStream) {
consumeState(quickCreateRuleStream);
},
);
}
}
@@ -2,6 +2,11 @@ import 'package:core/data/model/source_type/data_source_type.dart';
import 'package:get/get.dart';
import 'package:jmap_dart_client/http/http_client.dart';
import 'package:tmail_ui_user/features/base/base_bindings.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';
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/thread_detail/data/data_source/thread_detail_data_source.dart';
import 'package:tmail_ui_user/features/thread_detail/data/data_source/thread_detail_remote_data_source_impl.dart';
import 'package:tmail_ui_user/features/thread_detail/data/network/thread_detail_api.dart';
@@ -25,6 +30,11 @@ class ThreadDetailBindings extends BaseBindings {
Get.put(ThreadDetailController(
Get.find<GetThreadByIdInteractor>(),
Get.find<GetEmailsByIdsInteractor>(),
Get.find<MarkAsEmailReadInteractor>(),
Get.find<MarkAsStarEmailInteractor>(),
Get.find<PrintEmailInteractor>(),
Get.find<GetEmailContentInteractor>(),
Get.find<DownloadAttachmentForWebInteractor>(),
));
}
@@ -1,43 +1,88 @@
import 'package:flutter/material.dart';
import 'dart:async';
import 'package:core/data/network/download/download_manager.dart';
import 'package:core/presentation/state/failure.dart';
import 'package:core/presentation/state/success.dart';
import 'package:dartz/dartz.dart';
import 'package:get/get.dart';
import 'package:jmap_dart_client/jmap/account_id.dart';
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/email/individual_header_identifier.dart';
import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart';
import 'package:model/email/email_property.dart';
import 'package:model/email/presentation_email.dart';
import 'package:model/extensions/keyword_identifier_extension.dart';
import 'package:model/extensions/session_extension.dart';
import 'package:model/mailbox/presentation_mailbox.dart';
import 'package:tmail_ui_user/features/base/base_controller.dart';
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';
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/email/presentation/utils/email_action_reactor/email_action_reactor.dart';
import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart';
import 'package:tmail_ui_user/features/manage_account/domain/state/create_new_rule_filter_state.dart';
import 'package:tmail_ui_user/features/manage_account/domain/usecases/create_new_email_rule_filter_interactor.dart';
import 'package:tmail_ui_user/features/search/email/presentation/search_email_controller.dart';
import 'package:tmail_ui_user/features/thread_detail/domain/state/get_thread_by_id_state.dart';
import 'package:tmail_ui_user/features/thread_detail/domain/state/get_emails_by_ids_state.dart';
import 'package:tmail_ui_user/features/thread_detail/domain/usecases/get_thread_by_id_interactor.dart';
import 'package:tmail_ui_user/features/thread_detail/domain/usecases/get_emails_by_ids_interactor.dart';
import 'package:tmail_ui_user/features/thread_detail/presentation/action/thread_detail_ui_action.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/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/initialize_thread_detail_emails.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';
class ThreadDetailController extends BaseController {
final GetThreadByIdInteractor _getEmailIdsByThreadIdInteractor;
final GetEmailsByIdsInteractor getEmailsByIdsInteractor;
final MarkAsEmailReadInteractor _markAsEmailReadInteractor;
final MarkAsStarEmailInteractor _markAsStarEmailInteractor;
final PrintEmailInteractor _printEmailInteractor;
final GetEmailContentInteractor _getEmailContentInteractor;
final DownloadAttachmentForWebInteractor _downloadAttachmentForWebInteractor;
ThreadDetailController(
this._getEmailIdsByThreadIdInteractor,
this.getEmailsByIdsInteractor,
this._markAsEmailReadInteractor,
this._markAsStarEmailInteractor,
this._printEmailInteractor,
this._getEmailContentInteractor,
this._downloadAttachmentForWebInteractor,
);
final emailIdsPresentation = <EmailId, PresentationEmail?>{}.obs;
final currentExpandedEmailId = Rxn<EmailId>();
final currentEmailLoaded = Rxn<EmailLoaded>();
late final EmailActionReactor emailActionReactor;
final additionalProperties = Properties({
IndividualHeaderIdentifier.listPostHeader.value,
IndividualHeaderIdentifier.listUnsubscribeHeader.value,
});
final mailboxDashBoardController = Get.find<MailboxDashBoardController>();
final searchEmailController = Get.find<SearchEmailController>();
final downloadManager = Get.find<DownloadManager>();
final downloadProgressState = StreamController<Either<Failure, Success>>();
ScrollController? scrollController;
CreateNewEmailRuleFilterInteractor? _createNewEmailRuleFilterInteractor;
AccountId? get accountId => mailboxDashBoardController.accountId.value;
Session? get session => mailboxDashBoardController.sessionCurrent;
@@ -67,6 +112,17 @@ class ThreadDetailController extends BaseController {
@override
void onInit() {
super.onInit();
injectRuleFilterBindings(session, accountId);
_createNewEmailRuleFilterInteractor = getBinding<CreateNewEmailRuleFilterInteractor>();
emailActionReactor = EmailActionReactor(
_markAsEmailReadInteractor,
_markAsStarEmailInteractor,
_createNewEmailRuleFilterInteractor,
_printEmailInteractor,
_getEmailContentInteractor,
_downloadAttachmentForWebInteractor,
);
downloadProgressState.stream.listen(handleDownloadProgressState);
ever(mailboxDashBoardController.selectedEmail, (presentationEmail) {
if (presentationEmail?.threadId == null) {
closeThreadDetailAction(currentContext);
@@ -86,6 +142,22 @@ class ThreadDetailController extends BaseController {
));
}
});
ever(mailboxDashBoardController.threadDetailUIAction, (action) {
if (action is UpdatedEmailKeywordsAction) {
emailIdsPresentation
[action.emailId]
?.keywords
?[action.updatedKeyword] = action.value;
if (action.updatedKeyword == KeyWordIdentifierExtension.unsubscribeMail) {
emailIdsPresentation
[action.emailId]
?..emailHeader?.removeWhere((element) {
return element.name == EmailProperty.headerUnsubscribeKey;
})
..listUnsubscribeHeader?.clear();
}
}
});
}
void reset() {
@@ -103,6 +175,14 @@ class ThreadDetailController extends BaseController {
initializeThreadDetailEmails();
} else if (success is GetEmailsByIdsSuccess) {
handleGetEmailsByIdsSuccess(success);
} else if (success is MarkAsEmailReadSuccess) {
markCollapsedEmailReadSuccess(success);
} else if (success is MarkAsStarEmailSuccess) {
markCollapsedEmailStarSuccess(success);
} else if (success is CreateNewRuleFilterSuccess) {
quickCreateRuleFromCollapsedEmailSuccess(success);
} else if (success is DownloadAttachmentForWebSuccess) {
handleDownloadSuccess(success);
} else {
super.handleSuccessViewState(success);
}
@@ -118,6 +198,10 @@ class ThreadDetailController extends BaseController {
showRetryToast(failure);
return;
}
if (failure is DownloadAttachmentForWebFailure) {
handleDownloadFailure(failure);
return;
}
super.handleFailureViewState(failure);
}
}
@@ -4,18 +4,19 @@ 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/controller/single_email_controller.dart';
import 'package:tmail_ui_user/features/email/presentation/email_view.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/get_mailbox_contain_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_loading_view.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/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/routes/route_navigation.dart';
class ThreadDetailView extends GetWidget<ThreadDetailController> {
const ThreadDetailView({super.key});
@@ -39,15 +40,29 @@ class ThreadDetailView extends GetWidget<ThreadDetailController> {
lastEmailOfThread: controller.emailIdsPresentation.values.lastOrNull,
ownUserName: controller.session?.getOwnEmailAddress() ?? '',
mailboxContain: _getMailboxContain(),
emailLoaded: getBinding<SingleEmailController>(
tag: controller.emailIdsPresentation.keys.lastOrNull?.id.value
)?.currentEmailLoaded.value,
onEmailActionClick: (email, action) {
// TODO: Next PR
},
onMoreActionClick: (p0, p1) {
// TODO: Next PR
},
onEmailActionClick: controller.threadDetailOnEmailActionClick,
onMoreActionClick: (presentationEmail, position) => controller.emailActionReactor.handleMoreEmailAction(
mailboxContain: controller.mailboxDashBoardController.getMailboxContain(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,
],
),
);
}),
Obx(() => controller.getThreadDetailLoadingView(
@@ -5,10 +5,8 @@ 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/email_extension.dart';
import 'package:model/extensions/presentation_email_extension.dart';
import 'package:model/mailbox/presentation_mailbox.dart';
import 'package:tmail_ui_user/features/email/presentation/model/email_loaded.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';
@@ -28,7 +26,6 @@ class ThreadDetailAppBar extends StatelessWidget {
this.optionWidgets = const [],
this.onEmailActionClick,
this.onMoreActionClick,
this.emailLoaded,
});
final ResponsiveUtils responsiveUtils;
@@ -41,16 +38,31 @@ class ThreadDetailAppBar extends StatelessWidget {
final List<Widget> optionWidgets;
final OnEmailActionClick? onEmailActionClick;
final OnMoreActionClick? onMoreActionClick;
final EmailLoaded? emailLoaded;
@override
Widget build(BuildContext context) {
final isReplyToListEnabled = EmailUtils.isReplyToListEnabled(
emailLoaded?.emailCurrent?.listPost ?? '',
lastEmailOfThread?.listPost ?? '',
);
final child = LayoutBuilder(
builder: (context, constraints) {
Widget backButton = EmailViewBackButton(
imagePaths: imagePaths,
onBackAction: () => closeThreadDetailAction(context),
mailboxContain: mailboxContain,
isSearchActivated: isSearchRunning,
maxWidth: constraints.maxWidth,
);
if (responsiveUtils.isMobile(context)) {
backButton = Expanded(
child: Align(
alignment: Alignment.centerLeft,
child: backButton,
),
);
}
return Container(
height: PlatformInfo.isIOS
? EmailViewAppBarWidgetStyles.heightIOS(context, responsiveUtils)
@@ -76,20 +88,7 @@ class ThreadDetailAppBar extends StatelessWidget {
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
if (_supportDisplayMailboxNameTitle(context))
Expanded(
child: Align(
alignment: Alignment.centerLeft,
child: EmailViewBackButton(
imagePaths: imagePaths,
onBackAction: () => closeThreadDetailAction(context),
mailboxContain: mailboxContain,
isSearchActivated: isSearchRunning,
maxWidth: constraints.maxWidth,
),
),
),
...optionWidgets,
if (_supportDisplayMailboxNameTitle(context)) backButton,
if (lastEmailOfThread != null) ...[
TMailButtonWidget.fromIcon(
icon: imagePaths.icReply,
@@ -102,60 +101,45 @@ class ThreadDetailAppBar extends StatelessWidget {
EmailActionType.reply,
),
),
if (lastEmailOfThread!.getCountMailAddressWithoutMe(ownUserName) > 1)
if (!responsiveUtils.isMobile(context)) ...[
if (lastEmailOfThread!.getCountMailAddressWithoutMe(ownUserName) > 1)
TMailButtonWidget.fromIcon(
icon: imagePaths.icReplyAll,
iconSize: EmailViewAppBarWidgetStyles.buttonIconSize,
iconColor: EmailViewAppBarWidgetStyles.iconColor,
tooltipMessage: AppLocalizations.of(context).reply_all,
backgroundColor: Colors.transparent,
onTapActionCallback: () => onEmailActionClick?.call(
lastEmailOfThread!,
EmailActionType.replyAll,
),
),
if (isReplyToListEnabled)
TMailButtonWidget.fromIcon(
icon: imagePaths.icReply,
iconSize: EmailViewAppBarWidgetStyles.buttonIconSize,
iconColor: EmailViewAppBarWidgetStyles.iconColor,
tooltipMessage: AppLocalizations.of(context).replyToList,
backgroundColor: Colors.transparent,
onTapActionCallback: () => onEmailActionClick?.call(
lastEmailOfThread!,
EmailActionType.replyToList,
),
),
TMailButtonWidget.fromIcon(
icon: imagePaths.icReplyAll,
icon: imagePaths.icForward,
iconSize: EmailViewAppBarWidgetStyles.buttonIconSize,
iconColor: EmailViewAppBarWidgetStyles.iconColor,
tooltipMessage: AppLocalizations.of(context).reply_all,
tooltipMessage: AppLocalizations.of(context).forward,
backgroundColor: Colors.transparent,
onTapActionCallback: () => onEmailActionClick?.call(
lastEmailOfThread!,
EmailActionType.replyAll,
EmailActionType.forward,
),
),
if (isReplyToListEnabled)
TMailButtonWidget.fromIcon(
icon: imagePaths.icReply,
iconSize: EmailViewAppBarWidgetStyles.buttonIconSize,
iconColor: EmailViewAppBarWidgetStyles.iconColor,
tooltipMessage: AppLocalizations.of(context).replyToList,
backgroundColor: Colors.transparent,
onTapActionCallback: () => onEmailActionClick?.call(
lastEmailOfThread!,
EmailActionType.replyToList,
),
),
TMailButtonWidget.fromIcon(
icon: imagePaths.icForward,
iconSize: EmailViewAppBarWidgetStyles.buttonIconSize,
iconColor: EmailViewAppBarWidgetStyles.iconColor,
tooltipMessage: AppLocalizations.of(context).forward,
backgroundColor: Colors.transparent,
onTapActionCallback: () => onEmailActionClick?.call(
lastEmailOfThread!,
EmailActionType.forward,
),
),
TMailButtonWidget.fromIcon(
icon: lastEmailOfThread!.hasStarred
? imagePaths.icStar
: imagePaths.icUnStar,
iconSize: EmailViewAppBarWidgetStyles.buttonIconSize,
iconColor: lastEmailOfThread!.hasStarred
? null
: EmailViewAppBarWidgetStyles.iconColor,
backgroundColor: Colors.transparent,
tooltipMessage: lastEmailOfThread!.hasStarred
? AppLocalizations.of(context).not_starred
: AppLocalizations.of(context).mark_as_starred,
onTapActionCallback: () => onEmailActionClick?.call(
lastEmailOfThread!,
lastEmailOfThread!.hasStarred
? EmailActionType.unMarkAsStarred
: EmailActionType.markAsStarred
),
),
],
if (!responsiveUtils.isMobile(context)) const Spacer(),
...optionWidgets,
TMailButtonWidget.fromIcon(
icon: imagePaths.icMoreVertical,
iconSize: EmailViewAppBarWidgetStyles.buttonIconSize,
@@ -169,7 +153,7 @@ class ThreadDetailAppBar extends StatelessWidget {
? (position) => onMoreActionClick?.call(lastEmailOfThread!, position)
: null,
),
],
] else const Spacer(),
],
),
);
@@ -72,6 +72,7 @@ class ThreadDetailCollapsedEmail extends StatelessWidget {
openEmailAddressDetailAction: openEmailAddressDetailAction,
showRecipients: false,
onTapAvatarActionClick: onToggleThreadDetailCollapseExpand,
mailboxContain: mailboxContain,
),
Padding(
padding: const EdgeInsets.all(16),