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
@@ -245,7 +245,6 @@ extension AppColor on Color {
static const blue400 = Color(0xFF80BDFF);
static const blue900 = Color(0xFF0F76E7);
static const m3Tertiary = Color(0xFF8C9CAF);
static const lightIconTertiary = Color(0xFFB8C1CC);
static const m3Neutral70 = Color(0xFFAEAAAE);
static const m3Neutral90 = Color(0xFFE6E1E5);
static const grayBackgroundColor = Color(0xFFF3F6F9);
@@ -48,6 +48,11 @@ class TransformConfiguration {
const RemoveCollapsedSignatureButtonTransformer(),
]);
factory TransformConfiguration.forReplyForwardEmptyEmail() => TransformConfiguration.fromDomTransformers([
...TransformConfiguration.forReplyForwardEmail().domTransformers,
const ImageTransformer(),
]);
factory TransformConfiguration.forDraftsEmail() => TransformConfiguration.create(
customDomTransformers: [const ImageTransformer()]
);
@@ -196,7 +196,7 @@ extension EmailActionTypeExtension on EmailActionType {
case EmailActionType.openInNewTab:
return appLocalizations.openInNewTab;
case EmailActionType.printAll:
return appLocalizations.printAll;
return appLocalizations.print;
case EmailActionType.forward:
return appLocalizations.forward;
case EmailActionType.replyAll:
@@ -27,78 +27,13 @@ extension SetupEmailContentExtension on ComposerController {
switch(currentEmailActionType) {
case EmailActionType.editAsNewEmail:
case EmailActionType.editDraft:
final session = mailboxDashBoardController.sessionCurrent;
final accountId = mailboxDashBoardController.accountId.value;
final emailId = arguments.presentationEmail?.id;
if (session == null) {
emailContentsViewState.value = Left(GetEmailContentFailure(NotFoundSessionException()));
return;
}
if (accountId == null) {
emailContentsViewState.value = Left(GetEmailContentFailure(NotFoundAccountIdException()));
return;
}
if (emailId == null) {
emailContentsViewState.value = Left(GetEmailContentFailure(NotFoundEmailException()));
return;
}
final resultState = await getEmailContentInteractor.execute(
session,
accountId,
emailId,
mailboxDashBoardController.baseDownloadUrl,
TransformConfiguration.forEditDraftsEmail(),
await _getEmailContent(
arguments,
transformConfiguration: TransformConfiguration.forEditDraftsEmail(),
additionalProperties: Properties({
IndividualHeaderIdentifier.identityHeader.value,
}),
).last;
final uiState = resultState.fold((failure) => failure, (success) => success);
if (uiState is GetEmailContentSuccess) {
initAttachmentsAndInlineImages(
attachments: uiState.attachments,
inlineImages: uiState.inlineImages,
);
if (currentEmailActionType == EmailActionType.editDraft ||
currentEmailActionType == EmailActionType.editAsNewEmail) {
setupEmailRequestReadReceiptFlag(
uiState.emailCurrent!.hasRequestReadReceipt,
);
setupSelectedIdentityForEditDraft(
uiState.emailCurrent!.identityIdFromHeader,
);
}
emailContentsViewState.value = Right(uiState);
} else if (uiState is GetEmailContentFromCacheSuccess) {
initAttachmentsAndInlineImages(
attachments: uiState.attachments,
inlineImages: uiState.inlineImages,
);
if (currentEmailActionType == EmailActionType.editDraft ||
currentEmailActionType == EmailActionType.editAsNewEmail) {
setupEmailRequestReadReceiptFlag(
uiState.emailCurrent.hasRequestReadReceipt,
);
setupSelectedIdentityForEditDraft(
uiState.emailCurrent.identityIdFromHeader,
);
}
emailContentsViewState.value = Right(uiState);
} else if (uiState is GetEmailContentFailure) {
emailContentsViewState.value = Left(uiState);
consumeState(Stream.value(Left(uiState)));
} else {
emailContentsViewState.value = Right(LoadEmailContentCompleted());
}
);
break;
case EmailActionType.editSendingEmail:
final sendingEmail = arguments.sendingEmail;
@@ -130,7 +65,10 @@ extension SetupEmailContentExtension on ComposerController {
case EmailActionType.replyAll:
case EmailActionType.forward:
if (arguments.emailContents?.trim().isNotEmpty != true) {
emailContentsViewState.value = Left(GetEmailContentFailure(EmptyEmailContentException()));
await _getEmailContent(
arguments,
transformConfiguration: TransformConfiguration.forReplyForwardEmptyEmail(),
);
} else {
final resultState = await transformHtmlEmailContentInteractor.execute(
arguments.emailContents ?? '',
@@ -224,4 +162,81 @@ extension SetupEmailContentExtension on ComposerController {
break;
}
}
Future<void> _getEmailContent(
ComposerArguments arguments, {
required TransformConfiguration transformConfiguration,
Properties? additionalProperties,
}) async {
final session = mailboxDashBoardController.sessionCurrent;
final accountId = mailboxDashBoardController.accountId.value;
final emailId = arguments.presentationEmail?.id;
if (session == null) {
emailContentsViewState.value = Left(GetEmailContentFailure(NotFoundSessionException()));
return;
}
if (accountId == null) {
emailContentsViewState.value = Left(GetEmailContentFailure(NotFoundAccountIdException()));
return;
}
if (emailId == null) {
emailContentsViewState.value = Left(GetEmailContentFailure(NotFoundEmailException()));
return;
}
final resultState = await getEmailContentInteractor.execute(
session,
accountId,
emailId,
mailboxDashBoardController.baseDownloadUrl,
transformConfiguration,
additionalProperties: additionalProperties,
).last;
final uiState = resultState.fold((failure) => failure, (success) => success);
if (uiState is GetEmailContentSuccess) {
initAttachmentsAndInlineImages(
attachments: uiState.attachments,
inlineImages: uiState.inlineImages,
);
if (currentEmailActionType == EmailActionType.editDraft ||
currentEmailActionType == EmailActionType.editAsNewEmail) {
setupEmailRequestReadReceiptFlag(
uiState.emailCurrent!.hasRequestReadReceipt,
);
setupSelectedIdentityForEditDraft(
uiState.emailCurrent!.identityIdFromHeader,
);
}
emailContentsViewState.value = Right(uiState);
} else if (uiState is GetEmailContentFromCacheSuccess) {
initAttachmentsAndInlineImages(
attachments: uiState.attachments,
inlineImages: uiState.inlineImages,
);
if (currentEmailActionType == EmailActionType.editDraft
|| currentEmailActionType == EmailActionType.editAsNewEmail) {
setupEmailRequestReadReceiptFlag(
uiState.emailCurrent.hasRequestReadReceipt,
);
setupSelectedIdentityForEditDraft(
uiState.emailCurrent.identityIdFromHeader,
);
}
emailContentsViewState.value = Right(uiState);
} else if (uiState is GetEmailContentFailure) {
emailContentsViewState.value = Left(uiState);
consumeState(Stream.value(Left(uiState)));
} else {
emailContentsViewState.value = Right(LoadEmailContentCompleted());
}
}
}
@@ -1,9 +1,17 @@
import 'package:core/presentation/state/failure.dart';
import 'package:core/presentation/state/success.dart';
import 'package:jmap_dart_client/jmap/mail/email/email.dart';
class UnsubscribeEmailLoading extends LoadingState {}
class UnsubscribeEmailSuccess extends UIState {}
class UnsubscribeEmailSuccess extends UIState {
UnsubscribeEmailSuccess(this.emailId);
final EmailId emailId;
@override
List<Object?> get props => [emailId];
}
class UnsubscribeEmailFailure extends FeatureFailure {
@@ -16,7 +16,7 @@ class UnsubscribeEmailInteractor {
try {
yield Right(UnsubscribeEmailLoading());
await emailRepository.unsubscribeMail(session, accountId, emailId);
yield Right(UnsubscribeEmailSuccess());
yield Right(UnsubscribeEmailSuccess(emailId));
} catch (e) {
yield Left(UnsubscribeEmailFailure(exception: e));
}
@@ -43,11 +43,9 @@ import 'package:tmail_ui_user/features/base/base_controller.dart';
import 'package:tmail_ui_user/features/base/mixin/app_loader_mixin.dart';
import 'package:tmail_ui_user/features/base/state/button_state.dart';
import 'package:tmail_ui_user/features/composer/presentation/extensions/email_action_type_extension.dart';
import 'package:tmail_ui_user/features/destination_picker/presentation/model/destination_picker_arguments.dart';
import 'package:tmail_ui_user/features/email/domain/exceptions/email_exceptions.dart';
import 'package:tmail_ui_user/features/email/domain/extensions/list_attachments_extension.dart';
import 'package:tmail_ui_user/features/email/domain/model/detailed_email.dart';
import 'package:tmail_ui_user/features/email/domain/model/email_print.dart';
import 'package:tmail_ui_user/features/email/domain/model/event_action.dart';
import 'package:tmail_ui_user/features/email/domain/model/mark_read_action.dart';
import 'package:tmail_ui_user/features/email/domain/model/move_action.dart';
@@ -110,39 +108,35 @@ import 'package:tmail_ui_user/features/email/presentation/model/composer_argumen
import 'package:tmail_ui_user/features/email/presentation/model/email_loaded.dart';
import 'package:tmail_ui_user/features/email/presentation/model/email_unsubscribe.dart';
import 'package:tmail_ui_user/features/email/presentation/model/eml_previewer.dart';
import 'package:tmail_ui_user/features/email/presentation/utils/email_action_reactor/email_action_reactor.dart';
import 'package:tmail_ui_user/features/email/presentation/utils/email_utils.dart';
import 'package:tmail_ui_user/features/email/presentation/widgets/attachment_list/attachment_list_bottom_sheet_builder.dart';
import 'package:tmail_ui_user/features/email/presentation/widgets/attachment_list/attachment_list_dialog_builder.dart';
import 'package:tmail_ui_user/features/email/presentation/widgets/email_address_bottom_sheet_builder.dart';
import 'package:tmail_ui_user/features/email/presentation/widgets/email_address_dialog_builder.dart';
import 'package:tmail_ui_user/features/email/presentation/widgets/html_attachment_previewer.dart';
import 'package:tmail_ui_user/features/email/presentation/widgets/pdf_viewer/pdf_viewer.dart';
import 'package:tmail_ui_user/features/email_previewer/email_previewer_dialog_view.dart';
import 'package:tmail_ui_user/features/home/data/exceptions/session_exceptions.dart';
import 'package:tmail_ui_user/features/home/domain/extensions/session_extensions.dart';
import 'package:tmail_ui_user/features/mailbox/presentation/action/mailbox_ui_action.dart';
import 'package:tmail_ui_user/features/mailbox/presentation/model/mailbox_actions.dart';
import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart';
import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/extensions/get_mailbox_contain_extension.dart';
import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/extensions/open_and_close_composer_extension.dart';
import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/extensions/update_current_emails_flags_extension.dart';
import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/model/dashboard_routes.dart';
import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/model/download/download_task_state.dart';
import 'package:tmail_ui_user/features/manage_account/domain/model/create_new_email_rule_filter_request.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/state/get_all_identities_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/manage_account/domain/usecases/get_all_identities_interactor.dart';
import 'package:tmail_ui_user/features/manage_account/presentation/extensions/datetime_extension.dart';
import 'package:tmail_ui_user/features/rules_filter_creator/presentation/model/rules_filter_creator_arguments.dart';
import 'package:tmail_ui_user/features/search/email/presentation/search_email_controller.dart';
import 'package:tmail_ui_user/features/thread/presentation/model/delete_action_type.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/focus_thread_detail_expanded_email.dart';
import 'package:tmail_ui_user/features/thread_detail/presentation/thread_detail_controller.dart';
import 'package:tmail_ui_user/main/error/capability_validator.dart';
import 'package:tmail_ui_user/main/localizations/app_localizations.dart';
import 'package:tmail_ui_user/main/localizations/localization_service.dart';
import 'package:tmail_ui_user/main/routes/app_routes.dart';
import 'package:tmail_ui_user/main/routes/dialog_router.dart';
import 'package:tmail_ui_user/main/routes/navigation_router.dart';
import 'package:tmail_ui_user/main/routes/route_navigation.dart';
import 'package:tmail_ui_user/main/routes/route_utils.dart';
@@ -203,6 +197,7 @@ class SingleEmailController extends BaseController with AppLoaderMixin {
Identity? _identitySelected;
ButtonState? _printEmailButtonState;
final obxListeners = <Worker>[];
late final EmailActionReactor emailActionReactor;
final StreamController<Either<Failure, Success>> _downloadProgressStateController =
StreamController<Either<Failure, Success>>.broadcast();
@@ -254,6 +249,14 @@ class SingleEmailController extends BaseController with AppLoaderMixin {
_injectCalendarEventBindings(session, accountId);
_registerObxStreamListener();
_listenDownloadAttachmentProgressState();
emailActionReactor = EmailActionReactor(
_markAsEmailReadInteractor,
_markAsStarEmailInteractor,
_createNewEmailRuleFilterInteractor,
_printEmailInteractor,
_getEmailContentInteractor,
_downloadAttachmentForWebInteractor,
);
super.onInit();
}
@@ -408,6 +411,7 @@ class SingleEmailController extends BaseController with AppLoaderMixin {
obxListeners.add(ever(mailboxDashBoardController.viewState, (viewState) {
viewState.map((success) {
if (success is UnsubscribeEmailSuccess) {
if (success.emailId != _currentEmailId) return;
emailUnsubscribe.value = null;
}
});
@@ -766,13 +770,7 @@ class SingleEmailController extends BaseController with AppLoaderMixin {
}
PresentationMailbox? getMailboxContain(PresentationEmail email) {
if (mailboxDashBoardController.selectedMailbox.value == null) {
return email.findMailboxContain(mailboxDashBoardController.mapMailboxById);
} else {
return mailboxDashBoardController.searchController.isSearchEmailRunning
? email.findMailboxContain(mailboxDashBoardController.mapMailboxById)
: mailboxDashBoardController.selectedMailbox.value;
}
return mailboxDashBoardController.getMailboxContain(email);
}
void markAsEmailRead(
@@ -1200,81 +1198,28 @@ class SingleEmailController extends BaseController with AppLoaderMixin {
}
void moveToMailbox(BuildContext context, PresentationEmail email) async {
final currentMailbox = getMailboxContain(email);
if (currentMailbox != null && accountId != null) {
final arguments = DestinationPickerArguments(
if (session != null && accountId != null) {
final moveActionRequest = await emailActionReactor.moveToMailbox(
session!,
accountId!,
MailboxActions.moveEmail,
session,
mailboxIdSelected: currentMailbox.mailboxId
email,
mapMailbox: mailboxDashBoardController.mapMailboxById,
selectedMailbox: mailboxDashBoardController.selectedMailbox.value,
isSearchEmailRunning: mailboxDashBoardController.searchController.isSearchEmailRunning,
);
final destinationMailbox = PlatformInfo.isWeb
? await DialogRouter.pushGeneralDialog(routeName: AppRoutes.destinationPicker, arguments: arguments)
: await push(AppRoutes.destinationPicker, arguments: arguments);
if (destinationMailbox != null &&
destinationMailbox is PresentationMailbox &&
session != null &&
context.mounted
) {
_dispatchMoveToAction(
context,
accountId!,
session!,
email,
currentMailbox,
destinationMailbox);
if (!context.mounted || moveActionRequest == null) return;
mailboxDashBoardController.moveToMailbox(
session!,
accountId!,
moveActionRequest.moveRequest,
moveActionRequest.emailIdsWithReadStatus,
);
if (_threadDetailController?.emailIdsPresentation.length == 1) {
_threadDetailController?.closeThreadDetailAction(context);
}
}
}
void _dispatchMoveToAction(
BuildContext context,
AccountId accountId,
Session session,
PresentationEmail emailSelected,
PresentationMailbox currentMailbox,
PresentationMailbox destinationMailbox
) {
if (destinationMailbox.isTrash) {
_moveToTrashAction(
context,
session,
accountId,
MoveToMailboxRequest(
{currentMailbox.id: [emailSelected.id!]},
destinationMailbox.id,
MoveAction.moving,
EmailActionType.moveToTrash),
{emailSelected.id!: emailSelected.hasRead});
} else if (destinationMailbox.isSpam) {
_moveToSpamAction(
context,
session,
accountId,
MoveToMailboxRequest(
{currentMailbox.id: [emailSelected.id!]},
destinationMailbox.id,
MoveAction.moving,
EmailActionType.moveToSpam),
{emailSelected.id!: emailSelected.hasRead});
} else {
_moveToMailbox(
context,
session,
accountId,
MoveToMailboxRequest(
{currentMailbox.id: [emailSelected.id!]},
destinationMailbox.id,
MoveAction.moving,
EmailActionType.moveToMailbox,
destinationPath: destinationMailbox.mailboxPath),
{emailSelected.id!: emailSelected.hasRead});
}
}
void _moveToMailbox(
BuildContext context,
Session session,
@@ -1282,7 +1227,6 @@ class SingleEmailController extends BaseController with AppLoaderMixin {
MoveToMailboxRequest moveRequest,
Map<EmailId, bool> emailIdsWithReadStatus,
) {
closeEmailView(context: context);
consumeState(_moveToMailboxInteractor.execute(
session,
accountId,
@@ -1332,94 +1276,71 @@ class SingleEmailController extends BaseController with AppLoaderMixin {
}
void moveToTrash(BuildContext context, PresentationEmail email) {
final trashMailboxId = mailboxDashBoardController.getMailboxIdByRole(PresentationMailbox.roleTrash);
final currentMailbox = getMailboxContain(email);
if (session != null && accountId != null && currentMailbox != null && trashMailboxId != null) {
_moveToTrashAction(
context,
if (session != null && accountId != null) {
final moveActionRequest = emailActionReactor.moveToTrash(
email,
mapMailbox: mailboxDashBoardController.mapMailboxById,
selectedMailbox: mailboxDashBoardController.selectedMailbox.value,
isSearchEmailRunning: mailboxDashBoardController.searchController.isSearchEmailRunning,
mapDefaultMailboxIdByRole: mailboxDashBoardController.mapDefaultMailboxIdByRole,
);
if (!context.mounted || moveActionRequest == null) return;
mailboxDashBoardController.moveToMailbox(
session!,
accountId!,
MoveToMailboxRequest(
{currentMailbox.id: [email.id!]},
trashMailboxId,
MoveAction.moving,
EmailActionType.moveToTrash),
{email.id!: email.hasRead},
moveActionRequest.moveRequest,
moveActionRequest.emailIdsWithReadStatus,
);
if (_threadDetailController?.emailIdsPresentation.length == 1) {
_threadDetailController?.closeThreadDetailAction(context);
}
}
}
void _moveToTrashAction(
BuildContext context,
Session session,
AccountId accountId,
MoveToMailboxRequest moveRequest,
Map<EmailId, bool> emailIdsWithReadStatus,
) {
closeEmailView(context: context);
mailboxDashBoardController.moveToMailbox(
session,
accountId,
moveRequest,
emailIdsWithReadStatus,
);
}
void moveToSpam(BuildContext context, PresentationEmail email) {
final spamMailboxId = mailboxDashBoardController.spamMailboxId;
final currentMailbox = getMailboxContain(email);
if (session != null && accountId != null && currentMailbox != null && spamMailboxId != null) {
_moveToSpamAction(
context,
if (session != null && accountId != null) {
final moveActionRequest = emailActionReactor.moveToSpam(
email,
mapMailbox: mailboxDashBoardController.mapMailboxById,
selectedMailbox: mailboxDashBoardController.selectedMailbox.value,
isSearchEmailRunning: mailboxDashBoardController.searchController.isSearchEmailRunning,
mapDefaultMailboxIdByRole: mailboxDashBoardController.mapDefaultMailboxIdByRole,
);
if (!context.mounted || moveActionRequest == null) return;
mailboxDashBoardController.moveToMailbox(
session!,
accountId!,
MoveToMailboxRequest(
{currentMailbox.id: [email.id!]},
spamMailboxId,
MoveAction.moving,
EmailActionType.moveToSpam),
{email.id!: email.hasRead},
moveActionRequest.moveRequest,
moveActionRequest.emailIdsWithReadStatus,
);
if (_threadDetailController?.emailIdsPresentation.length == 1) {
_threadDetailController?.closeThreadDetailAction(context);
}
}
}
void unSpam(BuildContext context, PresentationEmail email) {
final spamMailboxId = mailboxDashBoardController.spamMailboxId;
final inboxMailboxId = mailboxDashBoardController.getMailboxIdByRole(PresentationMailbox.roleInbox);
if (session != null && accountId != null && spamMailboxId != null && inboxMailboxId != null) {
_moveToSpamAction(
context,
if (session != null && accountId != null) {
final moveActionRequest = emailActionReactor.unSpam(
email,
mapMailbox: mailboxDashBoardController.mapMailboxById,
selectedMailbox: mailboxDashBoardController.selectedMailbox.value,
isSearchEmailRunning: mailboxDashBoardController.searchController.isSearchEmailRunning,
mapDefaultMailboxIdByRole: mailboxDashBoardController.mapDefaultMailboxIdByRole,
);
if (!context.mounted || moveActionRequest == null) return;
mailboxDashBoardController.moveToMailbox(
session!,
accountId!,
MoveToMailboxRequest(
{spamMailboxId: [email.id!]},
inboxMailboxId,
MoveAction.moving,
EmailActionType.unSpam),
{email.id!: email.hasRead},
moveActionRequest.moveRequest,
moveActionRequest.emailIdsWithReadStatus,
);
if (_threadDetailController?.emailIdsPresentation.length == 1) {
_threadDetailController?.closeThreadDetailAction(context);
}
}
}
void _moveToSpamAction(
BuildContext context,
Session session,
AccountId accountId,
MoveToMailboxRequest moveRequest,
Map<EmailId, bool> emailIdsWithReadStatus,
) {
closeEmailView(context: context);
mailboxDashBoardController.moveToMailbox(
session,
accountId,
moveRequest,
emailIdsWithReadStatus,
);
}
void markAsStarEmail(
PresentationEmail presentationEmail,
MarkStarAction markStarAction,
@@ -1451,13 +1372,34 @@ class SingleEmailController extends BaseController with AppLoaderMixin {
void handleEmailAction(BuildContext context, PresentationEmail presentationEmail, EmailActionType actionType) {
switch(actionType) {
case EmailActionType.markAsUnread:
markAsEmailRead(presentationEmail, ReadActions.markAsUnread, MarkReadAction.tap);
if (session != null && accountId != null) {
consumeState(emailActionReactor.markAsEmailRead(
session!,
accountId!,
presentationEmail,
readAction: ReadActions.markAsUnread,
));
}
break;
case EmailActionType.markAsStarred:
markAsStarEmail(presentationEmail, MarkStarAction.markStar);
if (session != null && accountId != null) {
consumeState(emailActionReactor.markAsStarEmail(
session!,
accountId!,
presentationEmail,
markStarAction: MarkStarAction.markStar,
));
}
break;
case EmailActionType.unMarkAsStarred:
markAsStarEmail(presentationEmail, MarkStarAction.unMarkStar);
if (session != null && accountId != null) {
consumeState(emailActionReactor.markAsStarEmail(
session!,
accountId!,
presentationEmail,
markStarAction: MarkStarAction.unMarkStar,
));
}
break;
case EmailActionType.moveToMailbox:
moveToMailbox(context, presentationEmail);
@@ -1495,69 +1437,38 @@ class SingleEmailController extends BaseController with AppLoaderMixin {
case EmailActionType.reply:
_replyEmail(presentationEmail);
break;
case EmailActionType.replyAll:
case EmailActionType.replyToList:
case EmailActionType.forward:
pressEmailAction(actionType, presentationEmail);
break;
default:
break;
}
}
void openEmailAddressDialog(BuildContext context, EmailAddress emailAddress) {
if (responsiveUtils.isScreenWithShortestSide(context)) {
Get.bottomSheet(
PointerInterceptor(
child: EmailAddressBottomSheetBuilder(
imagePaths: imagePaths,
emailAddress: emailAddress,
onCloseDialogAction: popBack,
onCopyEmailAddressAction: (emailAddress) =>
copyEmailAddress(context, emailAddress),
onComposeEmailAction: composeEmailFromEmailAddress,
onQuickCreatingRuleEmailDialogAction: (emailAddress) =>
quickCreatingRule(context, emailAddress),
),
),
useRootNavigator: true,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadiusDirectional.only(
topStart: Radius.circular(16.0),
topEnd: Radius.circular(16.0),
),
),
isScrollControlled: true,
enableDrag: false,
backgroundColor: Colors.transparent,
);
} else {
Get.dialog(
PointerInterceptor(
child: EmailAddressDialogBuilder(
imagePaths: imagePaths,
emailAddress: emailAddress,
onCloseDialogAction: popBack,
onCopyEmailAddressAction: (emailAddress) =>
copyEmailAddress(context, emailAddress),
onComposeEmailAction: composeEmailFromEmailAddress,
onQuickCreatingRuleEmailDialogAction: (emailAddress) =>
quickCreatingRule(context, emailAddress),
),
),
barrierColor: AppColor.colorDefaultCupertinoActionSheet,
);
}
}
void openEmailAddressDialog(EmailAddress emailAddress) {
if (session == null || accountId == null) return;
void copyEmailAddress(BuildContext context, EmailAddress emailAddress) {
Clipboard.setData(ClipboardData(text: emailAddress.emailAddress));
appToast.showToastSuccessMessage(
context,
AppLocalizations.of(context).email_address_copied_to_clipboard,
emailActionReactor.openEmailAddressDialog(
session!,
accountId!,
emailAddress: emailAddress,
responsiveUtils: responsiveUtils,
imagePaths: imagePaths,
appToast: appToast,
onComposeEmailFromEmailAddressRequest: (emailAddress) {
popBack();
mailboxDashBoardController.openComposer(
ComposerArguments.fromEmailAddress(emailAddress),
);
},
onQuickCreateRuleRequest: (quickCreateRuleStream) {
consumeState(quickCreateRuleStream);
},
);
}
void composeEmailFromEmailAddress(EmailAddress emailAddress) {
popBack();
mailboxDashBoardController.openComposer(ComposerArguments.fromEmailAddress(emailAddress));
}
Future<void> openMailToLink(Uri? uri) async {
if (uri == null) return;
@@ -1577,34 +1488,18 @@ class SingleEmailController extends BaseController with AppLoaderMixin {
}
void deleteEmailPermanently(BuildContext context, PresentationEmail email) {
if (responsiveUtils.isMobile(context)) {
(ConfirmationDialogActionSheetBuilder(context)
..messageText(DeleteActionType.single.getContentDialog(context))
..onCancelAction(AppLocalizations.of(context).cancel, () => popBack())
..onConfirmAction(DeleteActionType.single.getConfirmActionName(context), () => _deleteEmailPermanentlyAction(context, email)))
.show();
} else {
Get.dialog(
PointerInterceptor(child: ConfirmationDialogBuilder(
key: const Key('confirm_dialog_delete_email_permanently'),
imagePath: imagePaths,
title: DeleteActionType.single.getTitleDialog(context),
textContent: DeleteActionType.single.getContentDialog(context),
confirmText: DeleteActionType.single.getConfirmActionName(context),
cancelText: AppLocalizations.of(context).cancel,
onConfirmButtonAction: () => _deleteEmailPermanentlyAction(context, email),
onCancelButtonAction: popBack,
onCloseButtonAction: popBack,
)),
barrierColor: AppColor.colorDefaultCupertinoActionSheet,
);
}
}
void _deleteEmailPermanentlyAction(BuildContext context, PresentationEmail email) {
popBack();
closeEmailView(context: context);
mailboxDashBoardController.deleteEmailPermanently(email);
emailActionReactor.deleteEmailPermanently(
email,
onDeleteEmailRequest: (email) {
popBack();
mailboxDashBoardController.deleteEmailPermanently(email);
if (_threadDetailController?.emailIdsPresentation.length == 1) {
_threadDetailController?.closeThreadDetailAction(context);
}
},
responsiveUtils: responsiveUtils,
imagePaths: imagePaths,
);
}
void _handleSendReceiptToSenderAction(BuildContext context) {
@@ -1793,32 +1688,11 @@ class SingleEmailController extends BaseController with AppLoaderMixin {
popBack();
if (accountId != null && session != null) {
final arguments = RulesFilterCreatorArguments(
accountId!,
consumeState(emailActionReactor.quickCreateRule(
session!,
emailAddress: emailAddress);
final newRuleFilterRequest = PlatformInfo.isWeb
? await DialogRouter.pushGeneralDialog(routeName: AppRoutes.rulesFilterCreator, arguments: arguments)
: await push(AppRoutes.rulesFilterCreator, arguments: arguments);
if (newRuleFilterRequest is CreateNewEmailRuleFilterRequest) {
_createNewRuleFilterAction(accountId!, newRuleFilterRequest);
}
}
}
void _createNewRuleFilterAction(
AccountId accountId,
CreateNewEmailRuleFilterRequest ruleFilterRequest
) async {
try {
_createNewEmailRuleFilterInteractor = Get.find<CreateNewEmailRuleFilterInteractor>();
} catch (e) {
logError('SingleEmailController::onInit(): ${e.toString()}');
}
if (_createNewEmailRuleFilterInteractor != null) {
consumeState(_createNewEmailRuleFilterInteractor!.execute(accountId, ruleFilterRequest));
accountId!,
emailAddress: emailAddress,
));
}
}
@@ -1929,70 +1803,30 @@ class SingleEmailController extends BaseController with AppLoaderMixin {
}
void _unsubscribeEmail(BuildContext context, PresentationEmail presentationEmail) {
showConfirmDialogAction(
context,
'',
AppLocalizations.of(context).unsubscribe,
onConfirmAction: () {
if (emailUnsubscribe.value?.httpLinks.isNotEmpty == true) {
_handleUnsubscribeMailByHttpsLink(
context: context,
emailId: presentationEmail.id!,
httpLinks: emailUnsubscribe.value!.httpLinks
);
} else if (emailUnsubscribe.value?.mailtoLinks.isNotEmpty == true) {
_handleUnsubscribeMailByMailtoLink(
context: context,
emailId: presentationEmail.id!,
mailtoLinks: emailUnsubscribe.value!.mailtoLinks
);
}
emailActionReactor.unsubscribeEmail(
presentationEmail,
emailUnsubscribe: emailUnsubscribe.value,
onUnsubscribeByHttpsLink: mailboxDashBoardController.unsubscribeMail,
onUnsubscribeByMailtoLink: (emailId, navigationRouter) {
mailboxDashBoardController.openComposer(
ComposerArguments.fromUnsubscribeMailtoLink(
listEmailAddress: navigationRouter.listEmailAddress,
subject: navigationRouter.subject,
body: navigationRouter.body,
previousEmailId: emailId,
)
);
},
showAsBottomSheet: true,
dialogMargin: MediaQuery.paddingOf(context).add(const EdgeInsets.only(bottom: 12)),
title: AppLocalizations.of(context).unsubscribeMail,
listTextSpan: [
TextSpan(text: AppLocalizations.of(context).unsubscribeMailDialogMessage),
TextSpan(
text: ' ${presentationEmail.getSenderName()}',
style: ThemeUtils.textStyleM3BodyMedium1.copyWith(
fontWeight: FontWeight.w700,
),
),
const TextSpan(text: ' ?'),
]
);
}
void _handleUnsubscribeMailByHttpsLink({
required BuildContext context,
required EmailId emailId,
required List<String> httpLinks
}) {
log('SingleEmailController::_handleUnsubscribeMailByHttpsLink:httpLinks: $httpLinks');
mailboxDashBoardController.unsubscribeMail(emailId);
AppUtils.launchLink(httpLinks.first);
}
void _handleUnsubscribeMailByMailtoLink({
required BuildContext context,
required EmailId emailId,
required List<String> mailtoLinks
}) {
log('SingleEmailController::_handleUnsubscribeMailByMailtoLink:mailtoLinks: $mailtoLinks');
final navigationRouter = RouteUtils.generateNavigationRouterFromMailtoLink(mailtoLinks.first);
mailboxDashBoardController.openComposer(
ComposerArguments.fromUnsubscribeMailtoLink(
listEmailAddress: navigationRouter.listEmailAddress,
subject: navigationRouter.subject,
body: navigationRouter.body,
previousEmailId: emailId,
)
);
}
void archiveMessage(BuildContext context, PresentationEmail email) {
mailboxDashBoardController.archiveMessage(context, email);
emailActionReactor.archiveMessage(
email,
onArchiveEmailRequest: (presentationEmail) {
mailboxDashBoardController.archiveMessage(context, presentationEmail);
},
);
}
void _printEmail(BuildContext context, PresentationEmail email) {
@@ -2003,40 +1837,11 @@ class SingleEmailController extends BaseController with AppLoaderMixin {
_printEmailButtonState = ButtonState.disabled;
final locale = Localizations.localeOf(context);
final appLocalizations = AppLocalizations.of(context);
consumeState(
_printEmailInteractor.execute(
EmailPrint(
appName: appLocalizations.app_name,
userName: mailboxDashBoardController.ownEmailAddress.value,
attachments: currentEmailLoaded.value!.attachments,
emailContent: currentEmailLoaded.value!.htmlContent,
fromPrefix: appLocalizations.from_email_address_prefix,
toPrefix: appLocalizations.to_email_address_prefix,
ccPrefix: appLocalizations.cc_email_address_prefix,
bccPrefix: appLocalizations.bcc_email_address_prefix,
replyToPrefix: appLocalizations.replyToEmailAddressPrefix,
titleAttachment: appLocalizations.attachments.toLowerCase(),
toAddress: email.to?.listEmailAddressToString(isFullEmailAddress: true),
ccAddress: email.cc?.listEmailAddressToString(isFullEmailAddress: true),
bccAddress: email.bcc?.listEmailAddressToString(isFullEmailAddress: true),
replyToAddress: email.replyTo?.listEmailAddressToString(isFullEmailAddress: true),
sender: email.from?.isNotEmpty == true
? email.from!.first
: null,
receiveTime: email.getReceivedAt(
locale.toLanguageTag(),
pattern: email.receivedAt
?.value
.toLocal()
.toPatternForPrinting(locale.toLanguageTag()),
),
subject: email.subject,
)
)
);
consumeState(emailActionReactor.printEmail(
email,
ownEmailAddress: mailboxDashBoardController.ownEmailAddress.value,
emailLoaded: currentEmailLoaded.value!,
));
}
void _showMessageWhenStartingEmailPrinting() {
@@ -2181,34 +1986,46 @@ class SingleEmailController extends BaseController with AppLoaderMixin {
}
void _downloadMessageAsEML(PresentationEmail presentationEmail) {
final emlAttachment = presentationEmail.createEMLAttachment();
if (emlAttachment.blobId == null) {
consumeState(Stream.value(Left(DownloadAttachmentForWebFailure(exception: NotFoundEmailBlobIdException()))));
return;
}
if (accountId == null || session == null) return;
downloadAttachmentForWeb(emlAttachment);
consumeState(emailActionReactor.downloadMessageAsEML(
session!,
accountId!,
presentationEmail,
downloadProgressStateController: _downloadProgressStateController,
));
}
void _editAsNewEmail(PresentationEmail presentationEmail) {
if (accountId == null || session == null) return;
mailboxDashBoardController.openComposer(
ComposerArguments.editAsNewEmail(presentationEmail),
emailActionReactor.editAsNewEmail(
presentationEmail,
onEditAsEmailRequest: (presentationEmail) {
mailboxDashBoardController.openComposer(
ComposerArguments.editAsNewEmail(presentationEmail),
);
},
);
}
void _replyEmail(PresentationEmail presentationEmail) {
mailboxDashBoardController.openComposer(
ComposerArguments.replyEmail(
presentationEmail: presentationEmail,
content: currentEmailLoaded.value?.htmlContent ?? '',
inlineImages: currentEmailLoaded.value?.inlineImages ?? [],
mailboxRole: presentationEmail.mailboxContain?.role,
messageId: currentEmailLoaded.value?.emailCurrent?.messageId,
references: currentEmailLoaded.value?.emailCurrent?.references,
listPost: currentEmailLoaded.value?.emailCurrent?.listPost,
)
emailActionReactor.replyEmail(
presentationEmail,
emailLoaded: currentEmailLoaded.value,
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,
)
);
},
);
}
+33 -127
View File
@@ -11,22 +11,15 @@ import 'package:jmap_dart_client/jmap/mail/calendar/calendar_event.dart';
import 'package:jmap_dart_client/jmap/mail/email/email.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/list_email_address_extension.dart';
import 'package:model/extensions/presentation_email_extension.dart';
import 'package:model/extensions/presentation_mailbox_extension.dart';
import 'package:model/mailbox/presentation_mailbox.dart';
import 'package:pointer_interceptor/pointer_interceptor.dart';
import 'package:tmail_ui_user/features/base/widget/optional_expanded.dart';
import 'package:tmail_ui_user/features/base/widget/optional_scroll.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/controller/single_email_controller.dart';
import 'package:tmail_ui_user/features/email/presentation/extensions/calendar_event_extension.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/email/presentation/model/email_loaded.dart';
import 'package:tmail_ui_user/features/email/presentation/styles/email_view_styles.dart';
import 'package:tmail_ui_user/features/email/presentation/utils/email_utils.dart';
import 'package:tmail_ui_user/features/email/presentation/widgets/calendar_event/calendar_event_action_banner_widget.dart';
import 'package:tmail_ui_user/features/email/presentation/widgets/calendar_event/calendar_event_detail_widget.dart';
import 'package:tmail_ui_user/features/email/presentation/widgets/calendar_event/calendar_event_information_widget.dart';
@@ -39,12 +32,10 @@ import 'package:tmail_ui_user/features/email/presentation/widgets/email_view_loa
import 'package:tmail_ui_user/features/email/presentation/widgets/information_sender_and_receiver_builder.dart';
import 'package:tmail_ui_user/features/email/presentation/widgets/mail_unsubscribed_banner.dart';
import 'package:tmail_ui_user/features/email/presentation/widgets/view_entire_message_with_message_clipped_widget.dart';
import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/extensions/handle_open_context_menu_extension.dart';
import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/extensions/verify_display_overlay_view_on_iframe_extension.dart';
import 'package:tmail_ui_user/features/manage_account/presentation/extensions/vacation_response_extension.dart';
import 'package:tmail_ui_user/features/manage_account/presentation/vacation/widgets/vacation_notification_message_widget.dart';
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/main/utils/app_utils.dart';
class EmailView extends GetWidget<SingleEmailController> {
@@ -97,11 +88,15 @@ class EmailView extends GetWidget<SingleEmailController> {
isSearchActivated: controller.mailboxDashBoardController.searchController.isSearchEmailRunning,
onBackAction: () => controller.closeEmailView(context: context),
onEmailActionClick: (email, action) => controller.handleEmailAction(context, email, action),
onMoreActionClick: (presentationEmail, position) => _handleMoreEmailAction(
context: context,
onMoreActionClick: (presentationEmail, position) => controller.emailActionReactor.handleMoreEmailAction(
mailboxContain: controller.getMailboxContain(presentationEmail),
presentationEmail: presentationEmail,
position: position,
emailLoaded: controller.currentEmailLoaded.value,
responsiveUtils: controller.responsiveUtils,
imagePaths: controller.imagePaths,
username: controller.session?.username,
handleEmailAction: (email, action) => controller.handleEmailAction(context, email, action),
additionalActions: [],
),
supportBackAction: !isInsideThreadDetailView,
appBarDecoration: isInsideThreadDetailView
@@ -278,18 +273,38 @@ class EmailView extends GetWidget<SingleEmailController> {
sMimeStatus: controller.currentEmailLoaded.value?.sMimeStatus,
emailUnsubscribe: controller.emailUnsubscribe.value,
maxBodyHeight: maxBodyHeight,
openEmailAddressDetailAction: controller.openEmailAddressDialog,
openEmailAddressDetailAction: (_, emailAddress) => controller.openEmailAddressDialog(emailAddress),
onEmailActionClick: (presentationEmail, actionType) => controller.handleEmailAction(context, presentationEmail, actionType),
isInsideThreadDetailView: isInsideThreadDetailView,
emailLoaded: controller.currentEmailLoaded.value,
onMoreActionClick: (email, position) => _handleMoreEmailAction(
context: context,
presentationEmail: email,
onMoreActionClick: (presentationEmail, position) => controller.emailActionReactor.handleMoreEmailAction(
mailboxContain: controller.getMailboxContain(presentationEmail),
presentationEmail: presentationEmail,
position: position,
emailLoaded: controller.currentEmailLoaded.value,
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,
],
],
),
onToggleThreadDetailCollapseExpand: onToggleThreadDetailCollapseExpand,
onTapAvatarActionClick: onToggleThreadDetailCollapseExpand,
mailboxContain: presentationEmail.findMailboxContain(
controller.mailboxDashBoardController.mapMailboxById,
),
)),
Obx(() => MailUnsubscribedBanner(
presentationEmail: controller.currentEmail,
@@ -340,7 +355,7 @@ class EmailView extends GetWidget<SingleEmailController> {
.ownEmailAddress
.value,
onMailtoAttendeesAction: controller.handleMailToAttendees,
openEmailAddressDetailAction: controller.openEmailAddressDialog,
openEmailAddressDetailAction: (_, emailAddress) => controller.openEmailAddressDialog(emailAddress),
isFree: controller.isCalendarEventFree,
listEmailAddressSender: emailAddressSender ?? [],
isPortraitMobile: controller
@@ -496,113 +511,4 @@ class EmailView extends GetWidget<SingleEmailController> {
return usernameEvent.isNotEmpty && titleEvent.isNotEmpty;
}
bool canDeletePermanently(PresentationEmail email) {
return email.mailboxContain?.isTrash
?? email.mailboxContain?.isSpam
?? false;
}
void _handleMoreEmailAction({
required BuildContext context,
required PresentationEmail presentationEmail,
RelativeRect? position,
EmailLoaded? emailLoaded,
}) {
final mailboxContain = controller.getMailboxContain(presentationEmail);
final isChildOfTeamMailboxes = mailboxContain?.isChildOfTeamMailboxes ?? false;
final isSpam = mailboxContain?.isSpam ?? false;
final isArchive = mailboxContain?.isArchive ?? false;
final isTemplates = mailboxContain?.isTemplates ?? false;
final isUnsubscribeEnabled = !presentationEmail.isSubscribed &&
controller.emailUnsubscribe.value != null;
final isCreateRuleEnabled = presentationEmail.from?.isNotEmpty == true;
final isDownloadMessageAsEMLEnabled =
PlatformInfo.isWeb && PlatformInfo.isCanvasKit;
final emailActions = [
if (isInsideThreadDetailView) ...[
EmailActionType.forward,
if (presentationEmail.getCountMailAddressWithoutMe(controller.session?.username.value ?? '') > 1)
EmailActionType.replyAll,
if (EmailUtils.isReplyToListEnabled(emailLoaded?.emailCurrent?.listPost ?? ''))
EmailActionType.replyToList,
if (PlatformInfo.isWeb && PlatformInfo.isCanvasKit)
EmailActionType.printAll,
if (controller.responsiveUtils.isMobile(context))
EmailActionType.moveToMailbox,
if (!controller.responsiveUtils.isDesktop(context)) ...[
presentationEmail.hasStarred
? EmailActionType.unMarkAsStarred
: EmailActionType.markAsStarred,
canDeletePermanently(presentationEmail)
? EmailActionType.deletePermanently
: EmailActionType.moveToTrash,
],
],
EmailActionType.markAsUnread,
if (!isChildOfTeamMailboxes)
isSpam ? EmailActionType.unSpam : EmailActionType.moveToSpam,
if (isCreateRuleEnabled)
EmailActionType.createRule,
if (isUnsubscribeEnabled)
EmailActionType.unsubscribe,
if (!isArchive)
EmailActionType.archiveMessage,
if (isDownloadMessageAsEMLEnabled)
EmailActionType.downloadMessageAsEML,
if (!isTemplates)
EmailActionType.editAsNewEmail,
];
if (position == null) {
final contextMenuActions = emailActions
.map((action) => ContextItemEmailAction(
action,
AppLocalizations.of(context),
controller.imagePaths,
))
.toList();
controller.mailboxDashBoardController.openBottomSheetContextMenu(
context: context,
itemActions: contextMenuActions,
onContextMenuActionClick: (menuAction) {
popBack();
controller.handleEmailAction(
context,
presentationEmail,
menuAction.action,
);
},
);
} else {
final popupMenuItems = emailActions.map((actionType) {
return PopupMenuItem(
padding: EdgeInsets.zero,
child: PopupMenuItemActionWidget(
menuAction: PopupMenuItemEmailAction(
actionType,
AppLocalizations.of(context),
controller.imagePaths,
),
menuActionClick: (menuAction) {
popBack();
controller.handleEmailAction(
context,
presentationEmail,
menuAction.action,
);
},
),
);
}).toList();
controller.mailboxDashBoardController.openPopupMenu(
context,
position,
popupMenuItems,
);
}
}
}
@@ -144,8 +144,8 @@ class ComposerArguments extends RouterArguments {
factory ComposerArguments.replyEmail({
required PresentationEmail presentationEmail,
required String content,
required List<Attachment> inlineImages,
required String? content,
required List<Attachment>? inlineImages,
Role? mailboxRole,
MessageIdsHeaderValue? messageId,
MessageIdsHeaderValue? references,
@@ -163,8 +163,8 @@ class ComposerArguments extends RouterArguments {
factory ComposerArguments.replyToListEmail({
required PresentationEmail presentationEmail,
required String content,
required List<Attachment> inlineImages,
required String? content,
required List<Attachment>? inlineImages,
Role? mailboxRole,
MessageIdsHeaderValue? messageId,
MessageIdsHeaderValue? references,
@@ -182,8 +182,8 @@ class ComposerArguments extends RouterArguments {
factory ComposerArguments.replyAllEmail({
required PresentationEmail presentationEmail,
required String content,
required List<Attachment> inlineImages,
required String? content,
required List<Attachment>? inlineImages,
Role? mailboxRole,
MessageIdsHeaderValue? messageId,
MessageIdsHeaderValue? references,
@@ -201,9 +201,9 @@ class ComposerArguments extends RouterArguments {
factory ComposerArguments.forwardEmail({
required PresentationEmail presentationEmail,
required String content,
required List<Attachment> attachments,
required List<Attachment> inlineImages,
required String? content,
required List<Attachment>? attachments,
required List<Attachment>? inlineImages,
MessageIdsHeaderValue? messageId,
MessageIdsHeaderValue? references,
}) => ComposerArguments(
@@ -0,0 +1,886 @@
import 'dart:async';
import 'package:core/data/network/config/dynamic_url_interceptors.dart';
import 'package:core/domain/extensions/datetime_extension.dart';
import 'package:core/presentation/extensions/color_extension.dart';
import 'package:core/presentation/resources/image_paths.dart';
import 'package:core/presentation/state/failure.dart';
import 'package:core/presentation/state/success.dart';
import 'package:core/presentation/utils/app_toast.dart';
import 'package:core/presentation/utils/html_transformer/transform_configuration.dart';
import 'package:core/presentation/utils/responsive_utils.dart';
import 'package:core/presentation/utils/theme_utils.dart';
import 'package:core/presentation/views/bottom_popup/confirmation_dialog_action_sheet_builder.dart';
import 'package:core/presentation/views/dialog/confirmation_dialog_builder.dart';
import 'package:core/utils/app_logger.dart';
import 'package:core/utils/platform_info.dart';
import 'package:dartz/dartz.dart';
import 'package:dio/dio.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:get/get.dart';
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/core/user_name.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/mailbox/mailbox.dart';
import 'package:model/download/download_task_id.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_address_extension.dart';
import 'package:model/extensions/list_email_address_extension.dart';
import 'package:model/extensions/presentation_email_extension.dart';
import 'package:model/extensions/presentation_mailbox_extension.dart';
import 'package:model/extensions/session_extension.dart';
import 'package:model/mailbox/presentation_mailbox.dart';
import 'package:pointer_interceptor/pointer_interceptor.dart';
import 'package:tmail_ui_user/features/base/mixin/message_dialog_action_mixin.dart';
import 'package:tmail_ui_user/features/base/mixin/popup_context_menu_action_mixin.dart';
import 'package:tmail_ui_user/features/base/widget/popup_item_widget.dart';
import 'package:tmail_ui_user/features/composer/presentation/extensions/email_action_type_extension.dart';
import 'package:tmail_ui_user/features/destination_picker/presentation/model/destination_picker_arguments.dart';
import 'package:tmail_ui_user/features/email/domain/exceptions/email_exceptions.dart';
import 'package:tmail_ui_user/features/email/domain/model/email_print.dart';
import 'package:tmail_ui_user/features/email/domain/model/mark_read_action.dart';
import 'package:tmail_ui_user/features/email/domain/model/move_action.dart';
import 'package:tmail_ui_user/features/email/domain/model/move_to_mailbox_request.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/get_email_content_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/model/email_loaded.dart';
import 'package:tmail_ui_user/features/email/presentation/model/email_unsubscribe.dart';
import 'package:tmail_ui_user/features/email/presentation/utils/email_utils.dart';
import 'package:tmail_ui_user/features/email/presentation/widgets/email_action_cupertino_action_sheet_action_builder.dart';
import 'package:tmail_ui_user/features/email/presentation/widgets/email_address_bottom_sheet_builder.dart';
import 'package:tmail_ui_user/features/email/presentation/widgets/email_address_dialog_builder.dart';
import 'package:tmail_ui_user/features/mailbox/presentation/model/mailbox_actions.dart';
import 'package:tmail_ui_user/features/manage_account/domain/model/create_new_email_rule_filter_request.dart';
import 'package:tmail_ui_user/features/manage_account/domain/usecases/create_new_email_rule_filter_interactor.dart';
import 'package:tmail_ui_user/features/rules_filter_creator/presentation/model/rules_filter_creator_arguments.dart';
import 'package:tmail_ui_user/features/thread/presentation/model/delete_action_type.dart';
import 'package:tmail_ui_user/main/localizations/app_localizations.dart';
import 'package:tmail_ui_user/main/routes/app_routes.dart';
import 'package:tmail_ui_user/main/routes/dialog_router.dart';
import 'package:tmail_ui_user/main/routes/navigation_router.dart';
import 'package:tmail_ui_user/main/routes/route_navigation.dart';
import 'package:tmail_ui_user/main/routes/route_utils.dart';
import 'package:tmail_ui_user/main/utils/app_utils.dart';
import 'package:uuid/uuid.dart';
class EmailActionReactor with MessageDialogActionMixin, PopupContextMenuActionMixin {
const EmailActionReactor(
this._markAsEmailReadInteractor,
this._markAsStarEmailInteractor,
this._createNewEmailRuleFilterInteractor,
this._printEmailInteractor,
this._getEmailContentInteractor,
this._downloadAttachmentForWebInteractor,
);
final MarkAsEmailReadInteractor _markAsEmailReadInteractor;
final MarkAsStarEmailInteractor _markAsStarEmailInteractor;
final CreateNewEmailRuleFilterInteractor? _createNewEmailRuleFilterInteractor;
final PrintEmailInteractor _printEmailInteractor;
final GetEmailContentInteractor _getEmailContentInteractor;
final DownloadAttachmentForWebInteractor _downloadAttachmentForWebInteractor;
Stream<Either<Failure, Success>> markAsEmailRead(
Session session,
AccountId accountId,
PresentationEmail presentationEmail, {
required ReadActions readAction,
}) {
return _markAsEmailReadInteractor.execute(
session,
accountId,
presentationEmail.id!,
readAction,
MarkReadAction.tap,
presentationEmail.mailboxContain?.mailboxId,
);
}
Stream<Either<Failure, Success>> markAsStarEmail(
Session session,
AccountId accountId,
PresentationEmail presentationEmail, {
required MarkStarAction markStarAction,
}) {
return _markAsStarEmailInteractor.execute(
session,
accountId,
presentationEmail.id!,
markStarAction,
);
}
Future<({
MoveToMailboxRequest moveRequest,
Map<EmailId, bool> emailIdsWithReadStatus
})?> moveToMailbox(
Session session,
AccountId accountId,
PresentationEmail presentationEmail, {
required Map<MailboxId, PresentationMailbox> mapMailbox,
required PresentationMailbox? selectedMailbox,
required bool isSearchEmailRunning,
}) async {
final moveRequest = await _moveToMailbox(
session,
accountId,
presentationEmail,
mapMailbox: mapMailbox,
isSearchEmailRunning: isSearchEmailRunning,
selectedMailbox: selectedMailbox,
);
if (moveRequest == null) return null;
return (
moveRequest: moveRequest.moveRequest,
emailIdsWithReadStatus: moveRequest.emailIdsWithReadStatus,
);
}
({
MoveToMailboxRequest moveRequest,
Map<EmailId, bool> emailIdsWithReadStatus
})? moveToTrash(
PresentationEmail presentationEmail, {
required Map<MailboxId, PresentationMailbox> mapMailbox,
required PresentationMailbox? selectedMailbox,
required bool isSearchEmailRunning,
required Map<Role, MailboxId> mapDefaultMailboxIdByRole,
}) {
final trashMailboxId = mapDefaultMailboxIdByRole[
PresentationMailbox.roleTrash
];
final currentMailbox = _getMailboxContain(
presentationEmail,
mapMailbox: mapMailbox,
isSearchEmailRunning: isSearchEmailRunning,
selectedMailbox: selectedMailbox,
);
if (trashMailboxId == null || currentMailbox == null) return null;
return (
moveRequest: MoveToMailboxRequest(
{currentMailbox.id: [presentationEmail.id!]},
trashMailboxId,
MoveAction.moving,
EmailActionType.moveToTrash),
emailIdsWithReadStatus: {presentationEmail.id!: presentationEmail.hasRead},
);
}
void deleteEmailPermanently(
PresentationEmail presentationEmail, {
required void Function(PresentationEmail email) onDeleteEmailRequest,
required ResponsiveUtils responsiveUtils,
required ImagePaths imagePaths,
}) async {
if (currentContext == null || !currentContext!.mounted) return;
if (responsiveUtils.isMobile(currentContext!)) {
await (ConfirmationDialogActionSheetBuilder(currentContext!)
..messageText(DeleteActionType.single.getContentDialog(currentContext!))
..onCancelAction(AppLocalizations.of(currentContext!).cancel, () => popBack())
..onConfirmAction(
DeleteActionType.single.getConfirmActionName(currentContext!),
() => onDeleteEmailRequest.call(presentationEmail)
)
).show();
} else {
await Get.dialog(
PointerInterceptor(child: ConfirmationDialogBuilder(
key: const Key('confirm_dialog_delete_email_permanently'),
imagePath: imagePaths,
title: DeleteActionType.single.getTitleDialog(currentContext!),
textContent: DeleteActionType.single.getContentDialog(currentContext!),
cancelText: DeleteActionType.single.getConfirmActionName(currentContext!),
confirmText: AppLocalizations.of(currentContext!).cancel,
onCancelButtonAction: () => onDeleteEmailRequest.call(presentationEmail),
onConfirmButtonAction: popBack,
onCloseButtonAction: popBack,
)),
barrierColor: AppColor.colorDefaultCupertinoActionSheet,
);
}
}
({
MoveToMailboxRequest moveRequest,
Map<EmailId, bool> emailIdsWithReadStatus
})? moveToSpam(
PresentationEmail presentationEmail, {
required Map<MailboxId, PresentationMailbox> mapMailbox,
required PresentationMailbox? selectedMailbox,
required bool isSearchEmailRunning,
required Map<Role, MailboxId> mapDefaultMailboxIdByRole,
}) {
final spamMailboxId = _spamMailboxId(mapDefaultMailboxIdByRole);
final currentMailbox = _getMailboxContain(
presentationEmail,
mapMailbox: mapMailbox,
isSearchEmailRunning: isSearchEmailRunning,
selectedMailbox: selectedMailbox,
);
if (spamMailboxId == null || currentMailbox == null) return null;
return (
moveRequest: MoveToMailboxRequest(
{currentMailbox.id: [presentationEmail.id!]},
spamMailboxId,
MoveAction.moving,
EmailActionType.moveToSpam),
emailIdsWithReadStatus: {presentationEmail.id!: presentationEmail.hasRead},
);
}
({
MoveToMailboxRequest moveRequest,
Map<EmailId, bool> emailIdsWithReadStatus
})? unSpam(
PresentationEmail presentationEmail, {
required Map<MailboxId, PresentationMailbox> mapMailbox,
required PresentationMailbox? selectedMailbox,
required bool isSearchEmailRunning,
required Map<Role, MailboxId> mapDefaultMailboxIdByRole,
}) {
final spamMailboxId = _spamMailboxId(mapDefaultMailboxIdByRole);
final inboxMailboxId = mapDefaultMailboxIdByRole[
PresentationMailbox.roleInbox
];
if (spamMailboxId == null || inboxMailboxId == null) return null;
return (
moveRequest: MoveToMailboxRequest(
{spamMailboxId: [presentationEmail.id!]},
inboxMailboxId,
MoveAction.moving,
EmailActionType.unSpam
),
emailIdsWithReadStatus: {presentationEmail.id!: presentationEmail.hasRead},
);
}
MailboxId? _spamMailboxId(
Map<Role, MailboxId> mapDefaultMailboxIdByRole
) {
return mapDefaultMailboxIdByRole[
PresentationMailbox.roleSpam
] ?? mapDefaultMailboxIdByRole[
PresentationMailbox.roleJunk
];
}
Stream<Either<Failure, Success>> quickCreateRule(
Session session,
AccountId accountId, {
required EmailAddress? emailAddress,
}) async* {
Get.back();
final arguments = RulesFilterCreatorArguments(
accountId,
session,
emailAddress: emailAddress,
);
final newRuleFilterRequest = PlatformInfo.isWeb
? await DialogRouter.pushGeneralDialog(
routeName: AppRoutes.rulesFilterCreator,
arguments: arguments,
)
: await push(AppRoutes.rulesFilterCreator, arguments: arguments);
if (newRuleFilterRequest is! CreateNewEmailRuleFilterRequest) return;
yield* _createNewEmailRuleFilterInteractor?.execute(
accountId,
newRuleFilterRequest
) ?? const Stream.empty();
}
Future<void> unsubscribeEmail(
PresentationEmail presentationEmail, {
required EmailUnsubscribe? emailUnsubscribe,
required void Function(EmailId emailId) onUnsubscribeByHttpsLink,
required void Function(
EmailId emailId,
NavigationRouter navigationRouter,
) onUnsubscribeByMailtoLink,
}) async {
if (currentContext == null || !currentContext!.mounted) return;
await showConfirmDialogAction(
currentContext!,
'',
AppLocalizations.of(currentContext!).unsubscribe,
onConfirmAction: () {
if (emailUnsubscribe?.httpLinks.isNotEmpty == true) {
onUnsubscribeByHttpsLink(presentationEmail.id!);
AppUtils.launchLink(emailUnsubscribe!.httpLinks.first);
} else if (emailUnsubscribe?.mailtoLinks.isNotEmpty == true) {
final navigationRouter = RouteUtils.generateNavigationRouterFromMailtoLink(
emailUnsubscribe!.mailtoLinks.first,
);
onUnsubscribeByMailtoLink(presentationEmail.id!, navigationRouter);
}
},
showAsBottomSheet: true,
title: AppLocalizations.of(currentContext!).unsubscribeMail,
listTextSpan: [
TextSpan(text: AppLocalizations.of(currentContext!).unsubscribeMailDialogMessage),
TextSpan(
text: ' ${presentationEmail.getSenderName()}',
style: ThemeUtils.textStyleBodyBody2(
color: AppColor.steelGray400,
fontWeight: FontWeight.w700,
),
),
const TextSpan(text: ' ?'),
]
);
}
void archiveMessage(
PresentationEmail presentationEmail, {
required void Function(
PresentationEmail presentationEmail,
) onArchiveEmailRequest,
}) => onArchiveEmailRequest(presentationEmail);
Stream<Either<Failure, Success>> printEmail(
PresentationEmail presentationEmail, {
required String ownEmailAddress,
required EmailLoaded? emailLoaded,
VoidCallback? onGetEmailContentFailure,
Session? session,
AccountId? accountId,
String? baseDownloadUrl,
TransformConfiguration? transformConfiguration,
}) async* {
emailLoaded ??= await _getEmailLoaded(
session,
accountId,
presentationEmail.id,
baseDownloadUrl,
transformConfiguration,
);
if (emailLoaded == null) {
onGetEmailContentFailure?.call();
return;
}
if (currentContext == null || !currentContext!.mounted) return;
final locale = Localizations.localeOf(currentContext!);
final appLocalizations = AppLocalizations.of(currentContext!);
yield* _printEmailInteractor.execute(
EmailPrint(
appName: appLocalizations.app_name,
userName: ownEmailAddress,
attachments: emailLoaded.attachments,
emailContent: emailLoaded.htmlContent,
fromPrefix: appLocalizations.from_email_address_prefix,
toPrefix: appLocalizations.to_email_address_prefix,
ccPrefix: appLocalizations.cc_email_address_prefix,
bccPrefix: appLocalizations.bcc_email_address_prefix,
replyToPrefix: appLocalizations.replyToEmailAddressPrefix,
titleAttachment: appLocalizations.attachments.toLowerCase(),
toAddress: presentationEmail.to?.listEmailAddressToString(isFullEmailAddress: true),
ccAddress: presentationEmail.cc?.listEmailAddressToString(isFullEmailAddress: true),
bccAddress: presentationEmail.bcc?.listEmailAddressToString(isFullEmailAddress: true),
replyToAddress: presentationEmail.replyTo?.listEmailAddressToString(isFullEmailAddress: true),
sender: presentationEmail.from?.isNotEmpty == true
? presentationEmail.from!.first
: null,
receiveTime: presentationEmail.getReceivedAt(
locale.toLanguageTag(),
pattern: presentationEmail.receivedAt
?.value
.toLocal()
.toPatternForPrinting(locale.toLanguageTag()),
),
subject: presentationEmail.subject,
)
);
}
Stream<Either<Failure, Success>> downloadMessageAsEML(
Session session,
AccountId accountId,
PresentationEmail presentationEmail, {
required StreamController<Either<Failure, Success>> downloadProgressStateController,
}) async* {
final emlAttachment = presentationEmail.createEMLAttachment();
if (emlAttachment.blobId == null) {
yield* Stream.value(Left(DownloadAttachmentForWebFailure(
exception: NotFoundEmailBlobIdException(),
)));
return;
}
final generateTaskId = DownloadTaskId(const Uuid().v4());
try {
final baseDownloadUrl = session.getDownloadUrl(
jmapUrl: getBinding<DynamicUrlInterceptors>()?.jmapUrl,
);
final cancelToken = CancelToken();
yield* _downloadAttachmentForWebInteractor.execute(
generateTaskId,
emlAttachment,
accountId,
baseDownloadUrl,
downloadProgressStateController,
cancelToken: cancelToken,
previewerSupported: false,
);
} catch (e) {
logError('SingleEmailController::downloadAttachmentForWeb(): $e');
yield* Stream.value(Left(DownloadAttachmentForWebFailure(
attachment: emlAttachment,
taskId: generateTaskId,
exception: e,
)));
}
}
void editAsNewEmail(
PresentationEmail presentationEmail, {
required void Function(
PresentationEmail presentationEmail,
) onEditAsEmailRequest,
}) => onEditAsEmailRequest(presentationEmail);
void replyEmail(
PresentationEmail presentationEmail, {
required void Function(
PresentationEmail presentationEmail,
EmailLoaded? emailLoaded,
) onReplyEmailRequest,
required EmailLoaded? emailLoaded,
}) {
onReplyEmailRequest(presentationEmail, emailLoaded);
}
void replyAll(
PresentationEmail presentationEmail, {
required void Function(
PresentationEmail presentationEmail,
EmailLoaded? emailLoaded,
) onReplyAllRequest,
required EmailLoaded? emailLoaded,
}) {
onReplyAllRequest(presentationEmail, emailLoaded);
}
void replyToList(
PresentationEmail presentationEmail, {
required void Function(
PresentationEmail presentationEmail,
EmailLoaded? emailLoaded,
) onReplyToListRequest,
required EmailLoaded? emailLoaded,
}) {
onReplyToListRequest(presentationEmail, emailLoaded);
}
void forward(
PresentationEmail presentationEmail, {
required void Function(
PresentationEmail presentationEmail,
EmailLoaded? emailLoaded,
) onForwardRequest,
required EmailLoaded? emailLoaded,
}) {
onForwardRequest(presentationEmail, emailLoaded);
}
void handleMoreEmailAction({
required PresentationEmail presentationEmail,
required PresentationMailbox? mailboxContain,
required RelativeRect? position,
required ResponsiveUtils responsiveUtils,
required ImagePaths imagePaths,
required UserName? username,
required void Function(
PresentationEmail presentationEmail,
EmailActionType action,
) handleEmailAction,
required List<EmailActionType> additionalActions,
}) {
if (currentContext == null) return;
final moreActions = [
if (additionalActions.contains(EmailActionType.forward))
EmailActionType.forward,
if (presentationEmail.getCountMailAddressWithoutMe(username?.value ?? '') > 1 &&
additionalActions.contains(EmailActionType.replyAll))
EmailActionType.replyAll,
if (EmailUtils.isReplyToListEnabled(presentationEmail.listPost ?? '') &&
additionalActions.contains(EmailActionType.replyToList))
EmailActionType.replyToList,
if (PlatformInfo.isWeb &&
PlatformInfo.isCanvasKit &&
additionalActions.contains(EmailActionType.printAll))
EmailActionType.printAll,
if (additionalActions.contains(EmailActionType.moveToMailbox))
EmailActionType.moveToMailbox,
if (additionalActions.contains(EmailActionType.markAsStarred) &&
additionalActions.contains(EmailActionType.unMarkAsStarred))
presentationEmail.hasStarred
? EmailActionType.unMarkAsStarred
: EmailActionType.markAsStarred,
if (additionalActions.contains(EmailActionType.moveToTrash) &&
additionalActions.contains(EmailActionType.deletePermanently))
_canDeletePermanently(presentationEmail)
? EmailActionType.deletePermanently
: EmailActionType.moveToTrash,
EmailActionType.markAsUnread,
if (mailboxContain?.isChildOfTeamMailboxes == false)
if (mailboxContain?.isSpam == true)
EmailActionType.unSpam
else
EmailActionType.moveToSpam,
if (presentationEmail.from?.isNotEmpty == true)
EmailActionType.createRule,
if (!presentationEmail.isSubscribed && presentationEmail.listUnsubscribe != null)
EmailActionType.unsubscribe,
if (mailboxContain?.isArchive == false)
EmailActionType.archiveMessage,
if (PlatformInfo.isWeb && PlatformInfo.isCanvasKit)
EmailActionType.downloadMessageAsEML,
EmailActionType.editAsNewEmail,
];
if (position == null) {
openContextMenuAction(
currentContext!,
_emailActionMoreActionTile(
currentContext!,
presentationEmail,
moreActions,
responsiveUtils,
imagePaths,
handleEmailAction: handleEmailAction,
)
);
} else {
openPopupMenuAction(
currentContext!,
position,
_popupMenuEmailActionTile(
currentContext!,
presentationEmail,
moreActions,
imagePaths,
handleEmailAction: handleEmailAction,
)
);
}
}
bool _canDeletePermanently(PresentationEmail email) {
return email.mailboxContain?.isTrash
?? email.mailboxContain?.isSpam
?? false;
}
List<Widget> _emailActionMoreActionTile(
BuildContext context,
PresentationEmail presentationEmail,
List<EmailActionType> actionTypes,
ResponsiveUtils responsiveUtils,
ImagePaths imagePaths, {
required void Function(
PresentationEmail presentationEmail,
EmailActionType action,
) handleEmailAction
}) {
return actionTypes.map((action) {
return (EmailActionCupertinoActionSheetActionBuilder(
Key('${action.name}_action'),
SvgPicture.asset(
action.getIcon(imagePaths),
width: 24,
height: 24,
fit: BoxFit.fill,
colorFilter: AppColor.colorTextButton.asFilter()
),
action.getTitle(AppLocalizations.of(context)),
presentationEmail,
iconLeftPadding: responsiveUtils.isScreenWithShortestSide(context)
? const EdgeInsetsDirectional.only(start: 12, end: 16)
: const EdgeInsetsDirectional.only(end: 12),
iconRightPadding: responsiveUtils.isScreenWithShortestSide(context)
? const EdgeInsetsDirectional.only(end: 12)
: EdgeInsets.zero
)
..onActionClick((presentationEmail) {
popBack();
handleEmailAction(presentationEmail, action);
})).build();
}).toList();
}
List<PopupMenuEntry> _popupMenuEmailActionTile(
BuildContext context,
PresentationEmail presentationEmail,
List<EmailActionType> actionTypes,
ImagePaths imagePaths, {
required void Function(
PresentationEmail presentationEmail,
EmailActionType action,
) handleEmailAction,
}) {
return actionTypes.map((action) {
return PopupMenuItem(
key: Key('${action.name}_action'),
padding: EdgeInsets.zero,
child: PopupItemWidget(
iconAction: action.getIcon(imagePaths),
nameAction: action.getTitle(AppLocalizations.of(context)),
colorIcon: AppColor.colorTextButton,
padding: const EdgeInsetsDirectional.only(start: 12),
styleName: const TextStyle(
fontWeight: FontWeight.w500,
fontSize: 16,
color: Colors.black
),
onCallbackAction: () {
popBack();
handleEmailAction(presentationEmail, action);
}
)
);
}).toList();
}
void openEmailAddressDialog(
Session session,
AccountId accountId, {
required EmailAddress emailAddress,
required ResponsiveUtils responsiveUtils,
required ImagePaths imagePaths,
required AppToast appToast,
required void Function(EmailAddress emailAddress) onComposeEmailFromEmailAddressRequest,
required void Function(Stream<Either<Failure, Success>> quickCreateRuleStream) onQuickCreateRuleRequest,
}) {
if (currentContext?.mounted != true) return;
if (responsiveUtils.isScreenWithShortestSide(currentContext!)) {
Get.bottomSheet(
PointerInterceptor(
child: EmailAddressBottomSheetBuilder(
imagePaths: imagePaths,
emailAddress: emailAddress,
onCloseDialogAction: popBack,
onCopyEmailAddressAction: (emailAddress) =>
_copyEmailAddress(currentContext!, emailAddress, appToast),
onComposeEmailAction: (emailAddress) =>
onComposeEmailFromEmailAddressRequest(emailAddress),
onQuickCreatingRuleEmailDialogAction: (emailAddress) =>
onQuickCreateRuleRequest(
quickCreateRule(session, accountId, emailAddress: emailAddress),
),
),
),
useRootNavigator: true,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadiusDirectional.only(
topStart: Radius.circular(16.0),
topEnd: Radius.circular(16.0),
),
),
isScrollControlled: true,
enableDrag: false,
backgroundColor: Colors.transparent,
);
} else {
Get.dialog(
PointerInterceptor(
child: EmailAddressDialogBuilder(
imagePaths: imagePaths,
emailAddress: emailAddress,
onCloseDialogAction: popBack,
onCopyEmailAddressAction: (emailAddress) =>
_copyEmailAddress(currentContext!, emailAddress, appToast),
onComposeEmailAction: (emailAddress) =>
onComposeEmailFromEmailAddressRequest(emailAddress),
onQuickCreatingRuleEmailDialogAction: (emailAddress) =>
onQuickCreateRuleRequest(
quickCreateRule(session, accountId, emailAddress: emailAddress),
),
),
),
barrierColor: AppColor.colorDefaultCupertinoActionSheet,
);
}
}
void _copyEmailAddress(
BuildContext context,
EmailAddress emailAddress,
AppToast appToast,
) {
Clipboard.setData(ClipboardData(text: emailAddress.emailAddress));
appToast.showToastSuccessMessage(
context,
AppLocalizations.of(context).email_address_copied_to_clipboard,
);
}
Future<({
Map<EmailId, bool> emailIdsWithReadStatus,
MoveToMailboxRequest moveRequest,
})?> _moveToMailbox(
Session session,
AccountId accountId,
PresentationEmail presentationEmail, {
required Map<MailboxId, PresentationMailbox> mapMailbox,
PresentationMailbox? selectedMailbox,
bool isSearchEmailRunning = false,
}) async {
final currentMailbox = _getMailboxContain(
presentationEmail,
mapMailbox: mapMailbox,
selectedMailbox: selectedMailbox,
isSearchEmailRunning: isSearchEmailRunning,
);
if (currentMailbox == null) return null;
final arguments = DestinationPickerArguments(
accountId,
MailboxActions.moveEmail,
session,
mailboxIdSelected: currentMailbox.mailboxId
);
final destinationMailbox = PlatformInfo.isWeb
? await DialogRouter.pushGeneralDialog(routeName: AppRoutes.destinationPicker, arguments: arguments)
: await push(AppRoutes.destinationPicker, arguments: arguments);
if (destinationMailbox is PresentationMailbox) {
return _generateMoveMailboxRequest(
accountId,
session,
presentationEmail,
currentMailbox,
destinationMailbox,
);
}
return null;
}
PresentationMailbox? _getMailboxContain(
PresentationEmail presentationEmail, {
required Map<MailboxId, PresentationMailbox> mapMailbox,
PresentationMailbox? selectedMailbox,
bool isSearchEmailRunning = false,
}) {
if (selectedMailbox == null || isSearchEmailRunning) {
return presentationEmail.findMailboxContain(mapMailbox);
}
return selectedMailbox;
}
({
MoveToMailboxRequest moveRequest,
Map<EmailId, bool> emailIdsWithReadStatus,
}) _generateMoveMailboxRequest(
AccountId accountId,
Session session,
PresentationEmail emailSelected,
PresentationMailbox currentMailbox,
PresentationMailbox destinationMailbox
) {
if (destinationMailbox.isTrash) {
return (
moveRequest: MoveToMailboxRequest(
{currentMailbox.id: [emailSelected.id!]},
destinationMailbox.id,
MoveAction.moving,
EmailActionType.moveToTrash,
),
emailIdsWithReadStatus: {emailSelected.id!: emailSelected.hasRead},
);
} else if (destinationMailbox.isSpam) {
return (
moveRequest: MoveToMailboxRequest(
{currentMailbox.id: [emailSelected.id!]},
destinationMailbox.id,
MoveAction.moving,
EmailActionType.moveToSpam,
),
emailIdsWithReadStatus: {emailSelected.id!: emailSelected.hasRead},
);
} else {
return (
moveRequest: MoveToMailboxRequest(
{currentMailbox.id: [emailSelected.id!]},
destinationMailbox.id,
MoveAction.moving,
EmailActionType.moveToMailbox,
destinationPath: destinationMailbox.mailboxPath),
emailIdsWithReadStatus: {emailSelected.id!: emailSelected.hasRead},
);
}
}
Future<EmailLoaded?> _getEmailLoaded(
Session? session,
AccountId? accountId,
EmailId? emailId,
String? baseDownloadUrl,
TransformConfiguration? transformConfiguration,
) {
if (session == null ||
accountId == null ||
emailId == null ||
baseDownloadUrl == null ||
transformConfiguration == null
) {
return Future.value(null);
}
return _getEmailContentInteractor
.execute(
session,
accountId,
emailId,
baseDownloadUrl,
transformConfiguration,
)
.last
.then(
(value) {
return value.fold(
(failure) => null,
(success) => success is GetEmailContentSuccess
? EmailLoaded(
htmlContent: success.htmlEmailContent,
attachments: List.of(success.attachments ?? []),
inlineImages: List.of(success.inlineImages ?? []),
emailCurrent: success.emailCurrent,
)
: null,
);
},
onError: (error) {
logError('EmailActionReactor::_getEmailLoaded(): error: $error');
return null;
},
);
}
}
@@ -31,7 +31,6 @@ class EmailViewAppBarWidget extends StatelessWidget {
final BoxDecoration? appBarDecoration;
final EmailLoaded? emailLoaded;
final bool isInsideThreadDetailView;
final double? height;
final EdgeInsetsGeometry? iconPadding;
final EdgeInsetsGeometry? iconMargin;
final EdgeInsetsGeometry? padding;
@@ -49,7 +48,6 @@ class EmailViewAppBarWidget extends StatelessWidget {
this.supportBackAction = true,
this.appBarDecoration,
this.isInsideThreadDetailView = false,
this.height,
this.iconPadding,
this.iconMargin,
this.padding,
@@ -59,9 +57,9 @@ class EmailViewAppBarWidget extends StatelessWidget {
Widget build(BuildContext context) {
final child = LayoutBuilder(builder: (context, constraints) {
return Container(
height: height ?? (PlatformInfo.isIOS
height: PlatformInfo.isIOS
? EmailViewAppBarWidgetStyles.heightIOS(context, _responsiveUtils)
: EmailViewAppBarWidgetStyles.height),
: EmailViewAppBarWidgetStyles.height,
padding: padding ?? (PlatformInfo.isIOS
? EmailViewAppBarWidgetStyles.paddingIOS(context, _responsiveUtils)
: EmailViewAppBarWidgetStyles.padding),
@@ -248,7 +246,8 @@ class EmailViewAppBarWidget extends StatelessWidget {
}) {
if (!isInsideThreadDetailView) {
return [
_getPrintButton(appLocalizations),
if (PlatformInfo.isWeb && PlatformInfo.isCanvasKit)
_getPrintButton(appLocalizations),
_getMoveEmailButton(appLocalizations),
_getMarkStarButton(appLocalizations),
_getDeleteButton(appLocalizations),
@@ -5,15 +5,19 @@ import 'package:core/presentation/utils/responsive_utils.dart';
import 'package:core/presentation/utils/theme_utils.dart';
import 'package:core/presentation/views/button/tmail_button_widget.dart';
import 'package:core/presentation/views/image/avatar_builder.dart';
import 'package:core/utils/platform_info.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter_svg/flutter_svg.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/base/widget/email_avatar_builder.dart';
import 'package:tmail_ui_user/features/email/presentation/model/email_loaded.dart';
import 'package:tmail_ui_user/features/email/presentation/model/email_unsubscribe.dart';
import 'package:tmail_ui_user/features/email/presentation/model/smime_signature_status.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_receiver_widget.dart';
import 'package:tmail_ui_user/features/email/presentation/widgets/email_sender_builder.dart';
import 'package:tmail_ui_user/features/email/presentation/widgets/email_view_app_bar_widget.dart';
@@ -36,6 +40,7 @@ class InformationSenderAndReceiverBuilder extends StatelessWidget {
final bool showRecipients;
final VoidCallback? onToggleThreadDetailCollapseExpand;
final OnTapAvatarActionClick? onTapAvatarActionClick;
final PresentationMailbox? mailboxContain;
const InformationSenderAndReceiverBuilder({
Key? key,
@@ -53,6 +58,7 @@ class InformationSenderAndReceiverBuilder extends StatelessWidget {
this.showRecipients = true,
this.onToggleThreadDetailCollapseExpand,
this.onTapAvatarActionClick,
this.mailboxContain,
}) : super(key: key);
@override
@@ -127,21 +133,30 @@ class InformationSenderAndReceiverBuilder extends StatelessWidget {
padding: const EdgeInsetsDirectional.only(start: 16, top: 2),
),
if (isInsideThreadDetailView)
EmailViewAppBarWidget(
key: const Key('email_view_app_bar_widget'),
presentationEmail: emailSelected,
isSearchActivated: false,
onBackAction: () {},
onEmailActionClick: onEmailActionClick,
onMoreActionClick: onMoreActionClick,
supportBackAction: false,
appBarDecoration: const BoxDecoration(),
emailLoaded: emailLoaded,
isInsideThreadDetailView: isInsideThreadDetailView,
SizedBox(
height: IconUtils.defaultIconSize,
iconPadding: EdgeInsets.zero,
iconMargin: const EdgeInsetsDirectional.only(start: 16),
padding: const EdgeInsets.symmetric(horizontal: 8),
child: OverflowBox(
maxHeight: PlatformInfo.isIOS
? EmailViewAppBarWidgetStyles.heightIOS(context, responsiveUtils)
: EmailViewAppBarWidgetStyles.height,
fit: OverflowBoxFit.deferToChild,
child: EmailViewAppBarWidget(
key: const Key('email_view_app_bar_widget'),
presentationEmail: emailSelected,
mailboxContain: mailboxContain,
isSearchActivated: false,
onBackAction: () {},
onEmailActionClick: onEmailActionClick,
onMoreActionClick: onMoreActionClick,
supportBackAction: false,
appBarDecoration: const BoxDecoration(),
emailLoaded: emailLoaded,
isInsideThreadDetailView: isInsideThreadDetailView,
iconPadding: const EdgeInsets.all(8),
iconMargin: EdgeInsets.zero,
padding: EdgeInsets.zero,
),
),
),
],
),
@@ -151,10 +151,10 @@ class MailboxDashBoardBindings extends BaseBindings {
SendingQueueBindings().dependencies();
MailboxBindings().dependencies();
ThreadBindings().dependencies();
SearchEmailBindings().dependencies();
ThreadDetailBindings().dependencies();
EmailBindings().dependencies();
SearchEmailBindings().dependencies();
QuotasBindings().dependencies();
ThreadDetailBindings().dependencies();
}
@override
@@ -174,6 +174,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/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';
import 'package:tmail_ui_user/main/deep_links/open_app_deep_link_data.dart';
@@ -252,6 +253,7 @@ class MailboxDashBoardController extends ReloadableController
final dashBoardAction = Rxn<UIAction>();
final mailboxUIAction = Rxn<MailboxUIAction>();
final emailUIAction = Rxn<EmailUIAction>();
final threadDetailUIAction = Rxn<ThreadDetailUIAction>();
final dashboardRoute = DashboardRoutes.waiting.obs;
final currentSelectMode = SelectMode.INACTIVE.obs;
final filterMessageOption = FilterMessageOption.all.obs;
@@ -447,7 +449,7 @@ class MailboxDashBoardController extends ReloadableController
} else if (success is DeleteSendingEmailSuccess) {
getAllSendingEmails();
} else if (success is UnsubscribeEmailSuccess) {
_handleUnsubscribeMailSuccess();
_handleUnsubscribeMailSuccess(success.emailId);
} else if (success is RestoreDeletedMessageSuccess) {
_handleRestoreDeletedMessageSuccess(success.emailRecoveryAction.id!);
} else if (success is GetRestoredDeletedMessageSuccess) {
@@ -1766,6 +1768,11 @@ class MailboxDashBoardController extends ReloadableController
emailUIAction.value = newAction;
}
void dispatchThreadDetailUIAction(ThreadDetailUIAction newAction) {
log('MailboxDashBoardController::dispatchThreadDetailUIAction():newAction: ${newAction.runtimeType}');
threadDetailUIAction.value = newAction;
}
void dispatchRoute(DashboardRoutes route) {
log('MailboxDashBoardController::dispatchRoute(): $route');
dashboardRoute.value = route;
@@ -2848,16 +2855,29 @@ class MailboxDashBoardController extends ReloadableController
}
}
void _handleUnsubscribeMailSuccess() {
void _handleUnsubscribeMailSuccess(EmailId emailId) {
if (currentContext != null && currentOverlayContext != null) {
appToast.showToastSuccessMessage(
currentOverlayContext!,
AppLocalizations.of(currentContext!).unsubscribedFromThisMailingList);
}
final newEmail = selectedEmail.value?.updateKeywords({
dispatchThreadDetailUIAction(UpdatedEmailKeywordsAction(
emailId,
KeyWordIdentifierExtension.unsubscribeMail,
true,
));
// Reset threadDetailUIAction
dispatchThreadDetailUIAction(ThreadDetailUIAction());
final listEmail = searchController.isSearchEmailRunning
? listResultSearch
: emailsInCurrentMailbox;
var newEmailIndex = listEmail.indexWhere((email) => email.id == emailId);
if (newEmailIndex == -1) return;
listEmail[newEmailIndex] = listEmail[newEmailIndex].updateKeywords({
KeyWordIdentifierExtension.unsubscribeMail: true,
});
setSelectedEmail(newEmail);
}
void _replaceBrowserHistory({Uri? uri}) {
@@ -0,0 +1,16 @@
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/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart';
extension GetMailboxContainExtension on MailboxDashBoardController {
PresentationMailbox? getMailboxContain(PresentationEmail email) {
if (selectedMailbox.value == null) {
return email.findMailboxContain(mapMailboxById);
} else {
return searchController.isSearchEmailRunning
? email.findMailboxContain(mapMailboxById)
: selectedMailbox.value;
}
}
}
@@ -3,9 +3,9 @@ import 'package:jmap_dart_client/jmap/mail/email/keyword_identifier.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: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/presentation/action/thread_detail_ui_action.dart';
extension UpdateCurrentEmailsFlagsExtension on MailboxDashBoardController {
void updateEmailFlagByEmailIds(
@@ -76,23 +76,25 @@ extension UpdateCurrentEmailsFlagsExtension on MailboxDashBoardController {
}
void updateEmailAnswered(EmailId emailId) {
if (selectedEmail.value != null) {
final newEmail = selectedEmail.value!.updateKeywords({
KeyWordIdentifier.emailAnswered: true,
});
setSelectedEmail(newEmail);
}
dispatchThreadDetailUIAction(UpdatedEmailKeywordsAction(
emailId,
KeyWordIdentifier.emailAnswered,
true,
));
// Reset threadDetailUIAction
dispatchThreadDetailUIAction(ThreadDetailUIAction());
updateEmailFlagByEmailIds([emailId], markAsAnswered: true);
}
void updateEmailForwarded(EmailId emailId) {
if (selectedEmail.value != null) {
final newEmail = selectedEmail.value!.updateKeywords({
KeyWordIdentifier.emailForwarded: true,
});
setSelectedEmail(newEmail);
}
dispatchThreadDetailUIAction(UpdatedEmailKeywordsAction(
emailId,
KeyWordIdentifier.emailForwarded,
true,
));
// Reset threadDetailUIAction
dispatchThreadDetailUIAction(ThreadDetailUIAction());
updateEmailFlagByEmailIds([emailId], markAsForwarded: true);
}
@@ -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),
+17
View File
@@ -17,6 +17,7 @@ import 'package:model/email/email_in_thread_status.dart';
import 'package:model/email/mail_priority_header.dart';
import 'package:model/extensions/email_address_extension.dart';
import 'package:model/extensions/keyword_identifier_extension.dart';
import 'package:model/extensions/list_email_header_extension.dart';
import 'package:model/extensions/media_type_nullable_extension.dart';
import 'package:model/mailbox/presentation_mailbox.dart';
import 'package:model/mailbox/select_mode.dart';
@@ -50,6 +51,8 @@ class PresentationEmail with EquatableMixin, SearchSnippetMixin {
final Map<IndividualHeaderIdentifier, String?>? xPriorityHeader;
final Map<IndividualHeaderIdentifier, String?>? importanceHeader;
final Map<IndividualHeaderIdentifier, String?>? priorityHeader;
final Map<IndividualHeaderIdentifier, String?>? listPostHeader;
final Map<IndividualHeaderIdentifier, String?>? listUnsubscribeHeader;
final EmailInThreadStatus? emailInThreadStatus;
PresentationEmail({
@@ -79,6 +82,8 @@ class PresentationEmail with EquatableMixin, SearchSnippetMixin {
this.xPriorityHeader,
this.importanceHeader,
this.priorityHeader,
this.listPostHeader,
this.listUnsubscribeHeader,
this.emailInThreadStatus,
});
@@ -141,6 +146,12 @@ class PresentationEmail with EquatableMixin, SearchSnippetMixin {
bool get hasCalendarEvent => headerCalendarEvent?[IndividualHeaderIdentifier.headerCalendarEvent]?.isNotEmpty == true;
String? get listPost => emailHeader?.toSet().listPost?.trim()
?? listPostHeader?[IndividualHeaderIdentifier.listPostHeader]?.trim();
String? get listUnsubscribe => emailHeader?.toSet().listUnsubscribe?.trim()
?? listUnsubscribeHeader?[IndividualHeaderIdentifier.listUnsubscribeHeader]?.trim();
bool get isMarkAsImportant {
final xPriority = xPriorityHeader?[IndividualHeaderIdentifier.xPriorityHeader]
?.trim()
@@ -201,6 +212,8 @@ class PresentationEmail with EquatableMixin, SearchSnippetMixin {
xPriorityHeader,
importanceHeader,
priorityHeader,
listPostHeader,
listUnsubscribeHeader,
emailInThreadStatus,
];
@@ -231,6 +244,8 @@ class PresentationEmail with EquatableMixin, SearchSnippetMixin {
Map<IndividualHeaderIdentifier, String?>? xPriorityHeader,
Map<IndividualHeaderIdentifier, String?>? importanceHeader,
Map<IndividualHeaderIdentifier, String?>? priorityHeader,
Map<IndividualHeaderIdentifier, String?>? listPostHeader,
Map<IndividualHeaderIdentifier, String?>? listUnsubscribeHeader,
EmailInThreadStatus? emailInThreadStatus,
}) {
return PresentationEmail(
@@ -260,6 +275,8 @@ class PresentationEmail with EquatableMixin, SearchSnippetMixin {
xPriorityHeader: xPriorityHeader ?? this.xPriorityHeader,
importanceHeader: importanceHeader ?? this.importanceHeader,
priorityHeader: priorityHeader ?? this.priorityHeader,
listPostHeader: listPostHeader ?? this.listPostHeader,
listUnsubscribeHeader: listUnsubscribeHeader ?? this.listUnsubscribeHeader,
emailInThreadStatus: emailInThreadStatus ?? this.emailInThreadStatus,
);
}
+18 -2
View File
@@ -33,7 +33,9 @@ extension EmailExtension on Email {
bool get withAttachments => hasAttachment == true;
String get listUnsubscribe => headers.listUnsubscribe;
String get listUnsubscribe => headers.listUnsubscribe
?? listUnsubscribeHeader?[IndividualHeaderIdentifier.listUnsubscribeHeader]
?? '';
bool get hasRequestReadReceipt => headers.readReceiptHasBeenRequested ||
headerMdn?.containsKey(IndividualHeaderIdentifier.headerMdn) == true;
@@ -42,7 +44,9 @@ extension EmailExtension on Email {
String get sMimeStatusHeaderParsed => sMimeStatusHeader?[IndividualHeaderIdentifier.sMimeStatusHeader]?.trim() ?? '';
String get listPost => headers.listPost.trim();
String get listPost => headers.listPost?.trim()
?? listPostHeader?[IndividualHeaderIdentifier.listPostHeader]?.trim()
?? '';
IdentityId? get identityIdFromHeader {
final rawIdentityId = identityHeader?[IndividualHeaderIdentifier.identityHeader];
@@ -125,6 +129,8 @@ extension EmailExtension on Email {
xPriorityHeader: xPriorityHeader,
importanceHeader: importanceHeader,
priorityHeader: priorityHeader,
listPostHeader: listPostHeader,
listUnsubscribeHeader: listUnsubscribeHeader,
)
..searchSnippetSubject = searchSnippetSubject
..searchSnippetPreview = searchSnippetPreview;
@@ -158,6 +164,12 @@ extension EmailExtension on Email {
priorityHeader: updatedProperties.contain(IndividualHeaderIdentifier.priorityHeader.value)
? newEmail.priorityHeader
: priorityHeader,
listPostHeader: updatedProperties.contain(IndividualHeaderIdentifier.listPostHeader.value)
? newEmail.listPostHeader
: listPostHeader,
listUnsubscribeHeader: updatedProperties.contain(IndividualHeaderIdentifier.listUnsubscribeHeader.value)
? newEmail.listUnsubscribeHeader
: listUnsubscribeHeader,
);
}
@@ -246,6 +258,8 @@ extension EmailExtension on Email {
Map<IndividualHeaderIdentifier, String?>? xPriorityHeader,
Map<IndividualHeaderIdentifier, String?>? importanceHeader,
Map<IndividualHeaderIdentifier, String?>? priorityHeader,
Map<IndividualHeaderIdentifier, String?>? listPostHeader,
Map<IndividualHeaderIdentifier, String?>? listUnsubscribeHeader,
}) {
return Email(
id: id ?? this.id,
@@ -283,6 +297,8 @@ extension EmailExtension on Email {
xPriorityHeader: xPriorityHeader ?? this.xPriorityHeader,
importanceHeader: importanceHeader ?? this.importanceHeader,
priorityHeader: priorityHeader ?? this.priorityHeader,
listPostHeader: listPostHeader ?? this.listPostHeader,
listUnsubscribeHeader: listUnsubscribeHeader ?? this.listUnsubscribeHeader,
);
}
}
@@ -14,9 +14,9 @@ extension ListEmailHeaderExtension on Set<EmailHeader>? {
}
}
String get listUnsubscribe {
String? get listUnsubscribe {
final listUnsubscribe = this?.firstWhereOrNull((header) => header.name == EmailProperty.headerUnsubscribeKey);
return listUnsubscribe?.value ?? '';
return listUnsubscribe?.value;
}
String get sMimeStatus {
@@ -25,8 +25,8 @@ extension ListEmailHeaderExtension on Set<EmailHeader>? {
return sMimeStatus?.value.trim() ?? '';
}
String get listPost {
String? get listPost {
final listPost = this?.firstWhereOrNull((header) => header.name == EmailProperty.headerListPostKey);
return listPost?.value ?? '';
return listPost?.value;
}
}
@@ -111,6 +111,9 @@ extension PresentationEmailExtension on PresentationEmail {
xPriorityHeader: xPriorityHeader,
importanceHeader: importanceHeader,
priorityHeader: priorityHeader,
listPostHeader: listPostHeader,
listUnsubscribeHeader: listUnsubscribeHeader,
threadId: threadId,
);
}
@@ -2,6 +2,7 @@ import 'package:flutter_test/flutter_test.dart';
import 'package:get/get.dart';
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/properties/properties.dart';
import 'package:jmap_dart_client/jmap/mail/email/email.dart';
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';
@@ -40,6 +41,7 @@ void main() {
Map.fromEntries(emailIds.map((emailId) => MapEntry(emailId, null))).obs,
);
when(threadDetailController.getEmailsByIdsInteractor).thenReturn(getEmailsByIdsInteractor);
when(threadDetailController.additionalProperties).thenReturn(Properties.empty());
when(getEmailsByIdsInteractor.execute(
any,
any,
@@ -1,6 +1,7 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:get/get.dart';
import 'package:jmap_dart_client/jmap/core/id.dart';
import 'package:jmap_dart_client/jmap/core/properties/properties.dart';
import 'package:jmap_dart_client/jmap/mail/email/email.dart';
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';
@@ -37,6 +38,7 @@ void main() {
when(controller.session).thenReturn(SessionFixtures.aliceSession);
when(controller.accountId).thenReturn(AccountFixtures.aliceAccountId);
when(controller.getEmailsByIdsInteractor).thenReturn(getEmailsByIdsInteractor);
when(controller.additionalProperties).thenReturn(Properties.empty());
// Act
controller.loadMoreThreadDetailEmails();
@@ -86,6 +88,7 @@ void main() {
when(controller.session).thenReturn(SessionFixtures.aliceSession);
when(controller.accountId).thenReturn(AccountFixtures.aliceAccountId);
when(controller.getEmailsByIdsInteractor).thenReturn(getEmailsByIdsInteractor);
when(controller.additionalProperties).thenReturn(Properties.empty());
// Act
controller.loadMoreThreadDetailEmails();