TF-3515 Add save email as template feature

This commit is contained in:
DatDang
2025-02-25 16:10:20 +07:00
committed by Dat H. Pham
parent ef1f979c4b
commit 7f09f9ceb4
40 changed files with 1079 additions and 54 deletions
@@ -35,6 +35,7 @@ import 'package:tmail_ui_user/features/email/data/repository/email_repository_im
import 'package:tmail_ui_user/features/email/domain/repository/email_repository.dart';
import 'package:tmail_ui_user/features/email/domain/usecases/get_email_content_interactor.dart';
import 'package:tmail_ui_user/features/email/domain/usecases/print_email_interactor.dart';
import 'package:tmail_ui_user/features/email/domain/usecases/save_template_email_interactor.dart';
import 'package:tmail_ui_user/features/email/domain/usecases/transform_html_email_content_interactor.dart';
import 'package:tmail_ui_user/features/email/presentation/model/composer_arguments.dart';
import 'package:tmail_ui_user/features/mailbox/data/datasource/mailbox_datasource.dart';
@@ -303,6 +304,11 @@ class ComposerBindings extends BaseBindings {
IdentityInteractorsBindings(composerId: composerId).dependencies();
PreferencesInteractorsBindings(composerId: composerId).dependencies();
Get.lazyPut(() => SaveTemplateEmailInteractor(
Get.find<ComposerRepository>(tag: composerId),
Get.find<EmailRepository>(tag: composerId),
), tag: composerId);
}
@override
@@ -331,6 +337,7 @@ class ComposerBindings extends BaseBindings {
Get.find<CreateNewAndSaveEmailToDraftsInteractor>(tag: composerId),
Get.find<PrintEmailInteractor>(tag: composerId),
Get.find<ComposerRepository>(tag: composerId),
Get.find<SaveTemplateEmailInteractor>(tag: composerId),
composerId: composerId,
composerArgs: composerArguments,
), tag: composerId);
@@ -16,10 +16,12 @@ import 'package:flutter/services.dart';
import 'package:get/get.dart';
import 'package:html_editor_enhanced/html_editor.dart';
import 'package:jmap_dart_client/jmap/account_id.dart';
import 'package:jmap_dart_client/jmap/core/error/method/error_method_response.dart';
import 'package:jmap_dart_client/jmap/core/session/session.dart';
import 'package:jmap_dart_client/jmap/identities/identity.dart';
import 'package:jmap_dart_client/jmap/mail/email/email.dart';
import 'package:jmap_dart_client/jmap/mail/email/email_address.dart';
import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart';
import 'package:model/model.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:pointer_interceptor/pointer_interceptor.dart';
@@ -32,6 +34,8 @@ import 'package:tmail_ui_user/features/base/mixin/auto_complete_result_mixin.dar
import 'package:tmail_ui_user/features/base/state/base_ui_state.dart';
import 'package:tmail_ui_user/features/base/state/button_state.dart';
import 'package:tmail_ui_user/features/composer/domain/exceptions/compose_email_exception.dart';
import 'package:tmail_ui_user/features/composer/domain/exceptions/set_method_exception.dart';
import 'package:tmail_ui_user/features/composer/domain/extensions/set_method_exception_description_extension.dart';
import 'package:tmail_ui_user/features/composer/domain/model/contact_suggestion_source.dart';
import 'package:tmail_ui_user/features/composer/domain/repository/composer_repository.dart';
import 'package:tmail_ui_user/features/composer/domain/state/download_image_as_base64_state.dart';
@@ -64,27 +68,33 @@ import 'package:tmail_ui_user/features/composer/presentation/extensions/setup_em
import 'package:tmail_ui_user/features/composer/presentation/extensions/setup_email_recipients_extension.dart';
import 'package:tmail_ui_user/features/composer/presentation/extensions/setup_email_request_read_receipt_flag_extension.dart';
import 'package:tmail_ui_user/features/composer/presentation/extensions/setup_email_subject_extension.dart';
import 'package:tmail_ui_user/features/composer/presentation/extensions/setup_email_template_id_extension.dart';
import 'package:tmail_ui_user/features/composer/presentation/extensions/setup_list_identities_extension.dart';
import 'package:tmail_ui_user/features/composer/presentation/extensions/setup_selected_identity_extension.dart';
import 'package:tmail_ui_user/features/composer/presentation/extensions/update_screen_display_mode_extension.dart';
import 'package:tmail_ui_user/features/composer/presentation/mixin/drag_drog_file_mixin.dart';
import 'package:tmail_ui_user/features/composer/presentation/model/create_email_request.dart';
import 'package:tmail_ui_user/features/composer/presentation/model/saved_email_draft.dart';
import 'package:tmail_ui_user/features/composer/presentation/model/inline_image.dart';
import 'package:tmail_ui_user/features/composer/presentation/model/prefix_recipient_state.dart';
import 'package:tmail_ui_user/features/composer/presentation/model/saved_composing_email.dart';
import 'package:tmail_ui_user/features/composer/presentation/model/screen_display_mode.dart';
import 'package:tmail_ui_user/features/composer/presentation/styles/composer_style.dart';
import 'package:tmail_ui_user/features/composer/presentation/view/editor_view_mixin.dart';
import 'package:tmail_ui_user/features/composer/presentation/widgets/mobile/from_composer_bottom_sheet_builder.dart';
import 'package:tmail_ui_user/features/composer/presentation/widgets/saving_message_dialog_view.dart';
import 'package:tmail_ui_user/features/composer/presentation/widgets/saving_template_dialog_view.dart';
import 'package:tmail_ui_user/features/composer/presentation/widgets/sending_message_dialog_view.dart';
import 'package:tmail_ui_user/features/email/domain/state/get_email_content_state.dart';
import 'package:tmail_ui_user/features/email/domain/state/save_template_email_state.dart';
import 'package:tmail_ui_user/features/email/domain/state/update_template_email_state.dart';
import 'package:tmail_ui_user/features/email/domain/usecases/get_email_content_interactor.dart';
import 'package:tmail_ui_user/features/email/domain/usecases/print_email_interactor.dart';
import 'package:tmail_ui_user/features/email/domain/usecases/save_template_email_interactor.dart';
import 'package:tmail_ui_user/features/email/domain/usecases/transform_html_email_content_interactor.dart';
import 'package:tmail_ui_user/features/email/presentation/extensions/presentation_email_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/domain/model/create_new_mailbox_request.dart';
import 'package:tmail_ui_user/features/mailbox_dashboard/domain/usecases/remove_composer_cache_by_id_on_web_interactor.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/open_and_close_composer_extension.dart';
@@ -157,6 +167,7 @@ class ComposerController extends BaseController
final ComposerRepository _composerRepository;
final String? composerId;
final ComposerArguments? composerArgs;
final SaveTemplateEmailInteractor _saveTemplateEmailInteractor;
GetAllAutoCompleteInteractor? _getAllAutoCompleteInteractor;
GetAutoCompleteInteractor? _getAutoCompleteInteractor;
@@ -226,6 +237,7 @@ class ComposerController extends BaseController
EmailActionType? currentEmailActionType;
EmailActionType? savedActionType;
int minInputLengthAutocomplete = AppConfig.defaultMinInputLengthAutocomplete;
EmailId? currentTemplateEmailId;
@visibleForTesting
int? get savedEmailDraftHash => _savedEmailDraftHash;
@@ -257,6 +269,7 @@ class ComposerController extends BaseController
this._createNewAndSaveEmailToDraftsInteractor,
this.printEmailInteractor,
this._composerRepository,
this._saveTemplateEmailInteractor,
{
this.composerId,
this.composerArgs,
@@ -528,6 +541,7 @@ class ComposerController extends BaseController
savedDraftHash: arguments.savedDraftHash ?? _savedEmailDraftHash,
savedActionType: savedActionType ?? currentEmailActionType,
savedEmailDraftId: emailIdEditing,
templateEmailId: currentTemplateEmailId,
);
}
@@ -627,6 +641,7 @@ class ComposerController extends BaseController
setupEmailAttachments(arguments);
setupEmailOtherComponents(arguments);
setupEmailRequestReadReceiptFlag(arguments);
setupEmailTemplateId(arguments);
await setupListIdentities(arguments);
await setupEmailContent(arguments);
@@ -1130,12 +1145,11 @@ class ComposerController extends BaseController
}
Future<bool> _validateEmailChange() async {
final newDraftHash = await _hashDraftEmail();
log('ComposerController::_validateEmailChange:newDraftHash = $newDraftHash | _savedEmailDraftHash = $_savedEmailDraftHash');
final newDraftHash = await _hashComposingEmail();
return _savedEmailDraftHash != newDraftHash;
}
Future<int> _hashDraftEmail() async {
Future<int> _hashComposingEmail() async {
String emailContent = await getContentInEditor();
emailContent = await _composerRepository.removeCollapsedExpandedSignatureEffect(
@@ -1149,7 +1163,7 @@ class ComposerController extends BaseController
);
}
final savedEmailDraft = SavedEmailDraft(
final savedEmailDraft = SavedComposingEmail(
subject: subjectEmail.value ?? '',
content: emailContent,
toRecipients: listToEmailAddress.toSet(),
@@ -1168,11 +1182,11 @@ class ComposerController extends BaseController
}
Future<void> _updateSavedEmailDraftHash() async {
_savedEmailDraftHash = await _hashDraftEmail();
_savedEmailDraftHash = await _hashComposingEmail();
}
Future<void> initEmailDraftHash() async {
final currentDraftHash = await _hashDraftEmail();
final currentDraftHash = await _hashComposingEmail();
final oldSavedDraftHash = composerArguments.value?.savedDraftHash;
@@ -1264,6 +1278,76 @@ class ComposerController extends BaseController
}
}
Future<void> handleClickSaveAsTemplateButton(BuildContext context) async {
if (composerArguments.value == null ||
mailboxDashBoardController.sessionCurrent == null ||
mailboxDashBoardController.accountId.value == null
) {
log('ComposerController::handleClickSaveAsTemplateButton: SESSION or ACCOUNT_ID or ARGUMENTS is NULL');
return;
}
MailboxId? templateMailboxId = mailboxDashBoardController
.getMailboxIdByRole(PresentationMailbox.roleTemplates);
templateMailboxId ??= mailboxDashBoardController.mapMailboxById
.where((_, mailbox) => mailbox.name?.name.toLowerCase() ==
PresentationMailbox.roleTemplates.value.toLowerCase())
.keys
.firstOrNull;
final emailContent = await getContentInEditor();
final cancelToken = CancelToken();
final resultState = await _showSavingMessageToTemplateDialog(
emailContent: emailContent,
templateMailboxId: templateMailboxId,
templateEmailId: currentTemplateEmailId,
createNewMailboxRequest: templateMailboxId != null
? null
: CreateNewMailboxRequest(
MailboxName(PresentationMailbox.roleTemplates.value.toUpperCase()),
),
cancelToken: cancelToken,
);
if (resultState is SaveTemplateEmailSuccess && context.mounted == true) {
currentTemplateEmailId = resultState.emailId;
appToast.showToastSuccessMessage(
context,
AppLocalizations.of(context).saveMessageToTemplateSuccess,
);
} else if (resultState is UpdateTemplateEmailSuccess && context.mounted == true) {
currentTemplateEmailId = resultState.emailId;
appToast.showToastSuccessMessage(
context,
AppLocalizations.of(context).updateMessageToTemplateSuccess,
);
} else if (resultState is SaveTemplateEmailFailure ||
resultState is UpdateTemplateEmailFailure ||
resultState is GenerateEmailFailure
) {
if (resultState.exception is BadCredentialsException) {
handleBadCredentialsException();
} else if (context.mounted) {
String message = '';
final exception = resultState.exception;
if (exception is SetMethodException) {
final exceptionDescription = exception.getDescriptionFromErrorType(
ErrorMethodResponse.invalidArguments,
);
message = exceptionDescription != null
? AppLocalizations.of(context).invalidArguments(exceptionDescription)
: AppLocalizations.of(context).saveMessageToTemplateFailed;
} else if (cancelToken.isCancelled) {
message = AppLocalizations.of(context).saveMessageToTemplateCancelled;
} else {
message = AppLocalizations.of(context).saveMessageToTemplateFailed;
}
appToast.showToastErrorMessage(context, message);
}
}
}
void clearFocus(BuildContext context) {
log('ComposerController::clearFocus:');
if (PlatformInfo.isMobile) {
@@ -2032,6 +2116,56 @@ class ComposerController extends BaseController
);
}
Future<dynamic> _showSavingMessageToTemplateDialog({
required String emailContent,
required MailboxId? templateMailboxId,
required EmailId? templateEmailId,
required CreateNewMailboxRequest? createNewMailboxRequest,
CancelToken? cancelToken,
}) {
final childWidget = PointerInterceptor(
child: SavingTemplateDialogView(
createEmailRequest: CreateEmailRequest(
session: mailboxDashBoardController.sessionCurrent!,
accountId: mailboxDashBoardController.accountId.value!,
emailActionType: composerArguments.value!.emailActionType,
subject: subjectEmail.value ?? '',
emailContent: emailContent,
fromSender: composerArguments.value!.presentationEmail?.from ?? {},
toRecipients: listToEmailAddress.toSet(),
ccRecipients: listCcEmailAddress.toSet(),
bccRecipients: listBccEmailAddress.toSet(),
replyToRecipients: listReplyToEmailAddress.toSet(),
hasRequestReadReceipt: hasRequestReadReceipt.value,
isMarkAsImportant: isMarkAsImportant.value,
identity: identitySelected.value,
attachments: uploadController.attachmentsUploaded,
inlineAttachments: uploadController.mapInlineAttachments,
sentMailboxId: getSentMailboxIdForComposer(),
templateMailboxId: templateMailboxId,
templateEmailId: templateEmailId,
answerForwardEmailId: composerArguments.value!.presentationEmail?.id,
unsubscribeEmailId: composerArguments.value!.previousEmailId,
messageId: composerArguments.value!.messageId,
references: composerArguments.value!.references,
emailSendingQueue: composerArguments.value!.sendingEmail,
displayMode: screenDisplayMode.value
),
saveTemplateEmailInteractor: _saveTemplateEmailInteractor,
createNewMailboxRequest: createNewMailboxRequest,
onCancel: (cancelToken) => cancelToken?.cancel(),
cancelToken: cancelToken,
),
);
return Get.dialog(
PlatformInfo.isMobile
? PopScope(canPop: false, child: childWidget)
: childWidget,
barrierDismissible: false,
barrierColor: AppColor.colorDefaultCupertinoActionSheet,
);
}
void _handleCancelSavingMessageToDrafts({CancelToken? cancelToken}) {
cancelToken?.cancel([SavingEmailToDraftsCanceledException()]);
}
@@ -563,6 +563,20 @@ class ComposerView extends GetWidget<ComposerController> {
}
)
),
PopupMenuItem(
padding: EdgeInsets.zero,
child: PopupItemWidget(
iconAction: controller.imagePaths.icSaveToDraft,
nameAction: AppLocalizations.of(context).saveAsTemplate,
colorIcon: ComposerStyle.popupItemIconColor,
styleName: ComposerStyle.popupItemTextStyle,
padding: ComposerStyle.popupItemPadding,
onCallbackAction: () {
popBack();
controller.handleClickSaveAsTemplateButton(context);
}
)
),
PopupMenuItem(
padding: EdgeInsets.zero,
child: PopupItemWidget(
@@ -66,6 +66,7 @@ class ComposerView extends GetWidget<ComposerController> {
toggleRequestReadReceiptAction: () => controller.toggleRequestReadReceipt(context),
toggleMarkAsImportantAction: () => controller.toggleMarkAsImportant(context),
saveToDraftsAction: () => controller.handleClickSaveAsDraftsButton(context),
saveToTemplateAction: () => controller.handleClickSaveAsTemplateButton(context),
deleteComposerAction: () => controller.handleClickDeleteComposer(context),
)),
ConstrainedBox(
@@ -576,6 +577,7 @@ class ComposerView extends GetWidget<ComposerController> {
printDraftAction: () => controller.printDraft(context),
toggleRequestReadReceiptAction: () => controller.toggleRequestReadReceipt(context),
toggleMarkAsImportantAction: () => controller.toggleMarkAsImportant(context),
saveAsTemplateAction: () => controller.handleClickSaveAsTemplateButton(context),
)),
],
),
@@ -885,6 +887,7 @@ class ComposerView extends GetWidget<ComposerController> {
printDraftAction: () => controller.printDraft(context),
toggleRequestReadReceiptAction: () => controller.toggleRequestReadReceipt(context),
toggleMarkAsImportantAction: () => controller.toggleMarkAsImportant(context),
saveAsTemplateAction: () => controller.handleClickSaveAsTemplateButton(context),
)),
],
),
@@ -39,12 +39,12 @@ extension CreateEmailRequestExtension on CreateEmailRequest {
}
}
Set<EmailAddress>? createReplyToRecipients({bool isDraft = false}) {
Set<EmailAddress>? createReplyToRecipients({bool isNotReplyTo = false}) {
if (replyToRecipients?.isNotEmpty == true) {
return replyToRecipients;
}
if (isDraft) return null;
if (isNotReplyTo) return null;
return identity?.replyTo?.isNotEmpty == true
? identity!.replyTo!
@@ -60,18 +60,27 @@ extension CreateEmailRequestExtension on CreateEmailRequest {
KeyWordIdentifier.emailDraft: true,
KeyWordIdentifier.emailSeen: true,
};
} else if (templateMailboxId != null) {
return {
KeyWordIdentifier.emailSeen: true,
};
} else {
return null;
}
}
Map<MailboxId, bool>? createMailboxIds() {
if (draftsMailboxId != null || outboxMailboxId != null) {
if (draftsMailboxId != null
|| outboxMailboxId != null
|| templateMailboxId != null
) {
return {
if (draftsMailboxId != null)
draftsMailboxId!: true,
if (outboxMailboxId != null)
outboxMailboxId!: true,
if (templateMailboxId != null)
templateMailboxId!: true,
};
} else {
return null;
@@ -115,6 +124,7 @@ extension CreateEmailRequestExtension on CreateEmailRequest {
required PartId partId,
bool withIdentityHeader = false,
bool isDraft = false,
bool isTemplate = false,
}) {
return Email(
mailboxIds: createMailboxIds(),
@@ -122,7 +132,7 @@ extension CreateEmailRequestExtension on CreateEmailRequest {
to: toRecipients,
cc: ccRecipients,
bcc: bccRecipients,
replyTo: createReplyToRecipients(isDraft: isDraft),
replyTo: createReplyToRecipients(isNotReplyTo: isDraft || isTemplate),
inReplyTo: createInReplyTo(),
references: createReferences(),
keywords: createKeywords(),
@@ -65,8 +65,9 @@ extension SetupEmailContentExtension on ComposerController {
inlineImages: uiState.inlineImages,
);
if (currentEmailActionType == EmailActionType.editDraft) {
setupEmailRequestReadReceiptFlagForEditDraft(
if (currentEmailActionType == EmailActionType.editDraft ||
currentEmailActionType == EmailActionType.editAsNewEmail) {
setupEmailRequestReadReceiptFlag(
uiState.emailCurrent!.hasRequestReadReceipt,
);
setupSelectedIdentityForEditDraft(
@@ -81,8 +82,9 @@ extension SetupEmailContentExtension on ComposerController {
inlineImages: uiState.inlineImages,
);
if (currentEmailActionType == EmailActionType.editDraft) {
setupEmailRequestReadReceiptFlagForEditDraft(
if (currentEmailActionType == EmailActionType.editDraft ||
currentEmailActionType == EmailActionType.editAsNewEmail) {
setupEmailRequestReadReceiptFlag(
uiState.emailCurrent.hasRequestReadReceipt,
);
setupSelectedIdentityForEditDraft(
@@ -10,7 +10,8 @@ extension SetupEmailRequestReadReceiptFlagExtension on ComposerController {
void setupEmailRequestReadReceiptFlag(ComposerArguments arguments) {
if (currentEmailActionType == EmailActionType.reopenComposerBrowser) {
hasRequestReadReceipt.value = arguments.hasRequestReadReceipt ?? false;
} else if (currentEmailActionType != EmailActionType.editDraft) {
} else if (currentEmailActionType != EmailActionType.editDraft &&
currentEmailActionType != EmailActionType.editAsNewEmail) {
getServerSetting();
}
}
@@ -3,7 +3,7 @@ import 'package:tmail_ui_user/features/composer/presentation/composer_controller
extension SetupEmailRequestReadReceiptFlagExtension on ComposerController {
void setupEmailRequestReadReceiptFlagForEditDraft(bool isRequestReadReceipt) {
void setupEmailRequestReadReceiptFlag(bool isRequestReadReceipt) {
hasRequestReadReceipt.value = isRequestReadReceipt;
}
}
@@ -0,0 +1,13 @@
import 'package:model/email/email_action_type.dart';
import 'package:tmail_ui_user/features/composer/presentation/composer_controller.dart';
import 'package:tmail_ui_user/features/email/presentation/model/composer_arguments.dart';
extension SetupEmailTemplateIdExtension on ComposerController {
void setupEmailTemplateId(ComposerArguments arguments) {
if (currentEmailActionType == EmailActionType.editAsNewEmail ||
currentEmailActionType == EmailActionType.reopenComposerBrowser
) {
currentTemplateEmailId = arguments.savedEmailTemplateId;
}
}
}
@@ -31,7 +31,9 @@ class CreateEmailRequest with EquatableMixin {
final MailboxId? outboxMailboxId;
final MailboxId? sentMailboxId;
final MailboxId? draftsMailboxId;
final MailboxId? templateMailboxId;
final EmailId? draftsEmailId;
final EmailId? templateEmailId;
final EmailId? answerForwardEmailId;
final EmailId? unsubscribeEmailId;
final MessageIdsHeaderValue? messageId;
@@ -64,7 +66,9 @@ class CreateEmailRequest with EquatableMixin {
this.outboxMailboxId,
this.sentMailboxId,
this.draftsMailboxId,
this.templateMailboxId,
this.draftsEmailId,
this.templateEmailId,
this.answerForwardEmailId,
this.unsubscribeEmailId,
this.messageId,
@@ -99,7 +103,9 @@ class CreateEmailRequest with EquatableMixin {
outboxMailboxId,
sentMailboxId,
draftsMailboxId,
templateMailboxId,
draftsEmailId,
templateEmailId,
answerForwardEmailId,
unsubscribeEmailId,
references,
@@ -6,10 +6,10 @@ import 'package:jmap_dart_client/jmap/mail/email/email_address.dart';
import 'package:json_annotation/json_annotation.dart';
import 'package:model/email/attachment.dart';
part 'saved_email_draft.g.dart';
part 'saved_composing_email.g.dart';
@JsonSerializable(includeIfNull: false, explicitToJson: true)
class SavedEmailDraft with EquatableMixin {
class SavedComposingEmail with EquatableMixin {
final String content;
final String subject;
final Set<EmailAddress> toRecipients;
@@ -21,7 +21,7 @@ class SavedEmailDraft with EquatableMixin {
final bool hasReadReceipt;
final bool isMarkAsImportant;
SavedEmailDraft({
SavedComposingEmail({
required this.content,
required this.subject,
required this.toRecipients,
@@ -34,12 +34,26 @@ class SavedEmailDraft with EquatableMixin {
this.isMarkAsImportant = false,
});
factory SavedEmailDraft.fromJson(Map<String, dynamic> json) => _$SavedEmailDraftFromJson(json);
factory SavedComposingEmail.fromJson(Map<String, dynamic> json) => _$SavedComposingEmailFromJson(json);
Map<String, dynamic> toJson() => _$SavedEmailDraftToJson(this);
Map<String, dynamic> toJson() => _$SavedComposingEmailToJson(this);
String asString() => jsonEncode(toJson());
factory SavedComposingEmail.empty() {
return SavedComposingEmail(
subject: '',
content: '',
toRecipients: {},
ccRecipients: {},
bccRecipients: {},
replyToRecipients: {},
attachments: [],
identity: null,
hasReadReceipt: false,
);
}
@override
List<Object?> get props => [
content,
@@ -0,0 +1,230 @@
import 'dart:async';
import 'dart:math';
import 'package:core/presentation/extensions/capitalize_extension.dart';
import 'package:core/presentation/extensions/color_extension.dart';
import 'package:core/presentation/state/failure.dart';
import 'package:core/presentation/state/success.dart';
import 'package:core/presentation/views/button/tmail_button_widget.dart';
import 'package:core/utils/app_logger.dart';
import 'package:dartz/dartz.dart' hide State;
import 'package:dio/dio.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:tmail_ui_user/features/composer/domain/state/generate_email_state.dart';
import 'package:tmail_ui_user/features/composer/presentation/model/create_email_request.dart';
import 'package:tmail_ui_user/features/email/domain/state/save_template_email_state.dart';
import 'package:tmail_ui_user/features/email/domain/state/update_template_email_state.dart';
import 'package:tmail_ui_user/features/email/domain/usecases/save_template_email_interactor.dart';
import 'package:tmail_ui_user/features/mailbox/domain/model/create_new_mailbox_request.dart';
import 'package:tmail_ui_user/main/localizations/app_localizations.dart';
import 'package:tmail_ui_user/main/routes/route_navigation.dart';
class SavingTemplateDialogView extends StatefulWidget {
final CreateEmailRequest createEmailRequest;
final SaveTemplateEmailInteractor saveTemplateEmailInteractor;
final CreateNewMailboxRequest? createNewMailboxRequest;
final CancelToken? cancelToken;
final void Function(CancelToken? cancelToken)? onCancel;
const SavingTemplateDialogView({
super.key,
required this.createEmailRequest,
required this.saveTemplateEmailInteractor,
required this.createNewMailboxRequest,
this.cancelToken,
this.onCancel,
});
@override
State<SavingTemplateDialogView> createState() => _SavingTemplateDialogViewState();
}
class _SavingTemplateDialogViewState extends State<SavingTemplateDialogView> {
StreamSubscription? _streamSubscription;
final ValueNotifier<Either<Failure, Success>?> _viewStateNotifier = ValueNotifier(null);
@override
void initState() {
super.initState();
_streamSubscription = widget.saveTemplateEmailInteractor
.execute(
createEmailRequest: widget.createEmailRequest,
createNewMailboxRequest: widget.createNewMailboxRequest,
cancelToken: widget.cancelToken
)
.listen(_handleDataStream, onError: _handleErrorStream);
}
void _handleDataStream(Either<Failure, Success> newState) {
_viewStateNotifier.value = newState;
newState.fold(
(failure) {
if (failure is SaveTemplateEmailFailure ||
failure is UpdateTemplateEmailFailure ||
failure is GenerateEmailFailure) {
popBack(result: failure);
}
},
(success) {
if (success is SaveTemplateEmailSuccess || success is UpdateTemplateEmailSuccess) {
popBack(result: success);
}
}
);
}
void _handleErrorStream(Object error, StackTrace stackTrace) {
logError('SavingTemplateDialogView::_handleErrorStream: Exception = $error');
popBack(result: SaveTemplateEmailFailure(exception: error));
}
@override
Widget build(BuildContext context) {
return Dialog(
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(12)),
),
insetPadding: const EdgeInsets.symmetric(horizontal: 24.0, vertical: 16.0),
alignment: Alignment.center,
child: Container(
decoration: const BoxDecoration(
borderRadius: BorderRadius.all(Radius.circular(12)),
color: Colors.white,
),
width: min(context.width, 400),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
clipBehavior: Clip.antiAlias,
padding: const EdgeInsetsDirectional.symmetric(vertical: 8, horizontal: 12),
decoration: const BoxDecoration(
borderRadius: BorderRadius.all(Radius.circular(12)),
color: AppColor.colorItemSelected,
),
alignment: Alignment.center,
child: Text(
AppLocalizations.of(context).savingTemplate.capitalizeFirstEach,
style: Theme.of(context).textTheme.labelLarge?.copyWith(
color: Colors.black,
fontWeight: FontWeight.bold,
fontSize: 17
),
),
),
const Divider(),
Column(
mainAxisSize: MainAxisSize.min,
children: [
Padding(
padding: const EdgeInsetsDirectional.only(start: 16, end: 16, top: 12, bottom: 4),
child: Row(
children: [
Text(
'${AppLocalizations.of(context).status}:',
style: Theme.of(context).textTheme.labelMedium?.copyWith(
color: Colors.black,
fontWeight: FontWeight.w500,
fontSize: 14
),
),
const SizedBox(width: 8),
Expanded(
child: ValueListenableBuilder(
valueListenable: _viewStateNotifier,
builder: (context, value, child) {
if (value == null) {
return child!;
}
return value.fold(
(failure) => child!,
(success) {
return Text(
'${_getStatusMessage(success)}...',
style: Theme.of(context).textTheme.labelSmall?.copyWith(
color: AppColor.labelColor,
fontSize: 14
),
);
}
);
},
child: Text(
'...',
style: Theme.of(context).textTheme.labelSmall?.copyWith(
color: AppColor.labelColor,
fontSize: 14
),
),
),
)
],
),
),
Padding(
padding: const EdgeInsetsDirectional.only(start: 16, end: 16, top: 4, bottom: 16),
child: Row(
children: [
Text(
'${AppLocalizations.of(context).progress}:',
style: Theme.of(context).textTheme.labelMedium?.copyWith(
color: Colors.black,
fontWeight: FontWeight.w500,
fontSize: 14
),
),
const SizedBox(width: 8),
Expanded(
child: LinearProgressIndicator(
color: Colors.white.withOpacity(0.6),
backgroundColor: AppColor.primaryColor,
borderRadius: const BorderRadius.all(Radius.circular(12)),
),
)
],
),
),
if (widget.onCancel != null)
Align(
alignment: AlignmentDirectional.centerEnd,
child: TMailButtonWidget.fromText(
text: AppLocalizations.of(context).cancel,
textStyle: Theme.of(context).textTheme.labelSmall?.copyWith(
color: Colors.black87,
fontSize: 15
),
padding: const EdgeInsetsDirectional.symmetric(horizontal: 20, vertical: 8),
margin: const EdgeInsetsDirectional.only(start: 12, end: 12, bottom: 16),
onTapActionCallback: () {
_viewStateNotifier.value = Left(SaveTemplateEmailFailure());
widget.onCancel!(widget.cancelToken);
},
),
)
],
)
],
),
),
);
}
String _getStatusMessage(Success success) {
if (success is GenerateEmailLoading) {
return AppLocalizations.of(context).creatingMessage;
} else {
return AppLocalizations.of(context).savingMessageToTemplateFolder;
}
}
@override
void dispose() {
_streamSubscription?.cancel();
_viewStateNotifier.dispose();
super.dispose();
}
}
@@ -28,6 +28,7 @@ class BottomBarComposerWidget extends StatelessWidget {
final VoidCallback toggleRequestReadReceiptAction;
final VoidCallback printDraftAction;
final VoidCallback toggleMarkAsImportantAction;
final VoidCallback saveAsTemplateAction;
const BottomBarComposerWidget({
super.key,
@@ -48,6 +49,7 @@ class BottomBarComposerWidget extends StatelessWidget {
required this.toggleRequestReadReceiptAction,
required this.printDraftAction,
required this.toggleMarkAsImportantAction,
required this.saveAsTemplateAction,
});
@override
@@ -158,6 +160,17 @@ class BottomBarComposerWidget extends StatelessWidget {
printDraftAction();
},
),
PopupItemWidget(
iconAction: imagePaths.icSaveToDraft,
nameAction: AppLocalizations.of(context).saveAsTemplate,
colorIcon: BottomBarComposerWidgetStyle.iconColor,
styleName: BottomBarComposerWidgetStyle.popupItemTextStyle,
padding: BottomBarComposerWidgetStyle.popupItemPadding,
onCallbackAction: () {
menuMoreOptionController.hideMenu();
saveAsTemplateAction();
},
),
],
arrangeAsList: true,
position: null,
@@ -28,6 +28,7 @@ class MobileResponsiveAppBarComposerWidget extends StatelessWidget {
final VoidCallback toggleRequestReadReceiptAction;
final VoidCallback printDraftAction;
final VoidCallback saveToDraftsAction;
final VoidCallback saveToTemplateAction;
final VoidCallback deleteComposerAction;
final VoidCallback toggleMarkAsImportantAction;
@@ -50,6 +51,7 @@ class MobileResponsiveAppBarComposerWidget extends StatelessWidget {
required this.toggleRequestReadReceiptAction,
required this.printDraftAction,
required this.saveToDraftsAction,
required this.saveToTemplateAction,
required this.deleteComposerAction,
required this.toggleMarkAsImportantAction,
});
@@ -186,6 +188,17 @@ class MobileResponsiveAppBarComposerWidget extends StatelessWidget {
saveToDraftsAction();
},
),
PopupItemWidget(
iconAction: imagePaths.icSaveToDraft,
nameAction: AppLocalizations.of(context).saveAsTemplate,
colorIcon: MobileAppBarComposerWidgetStyle.popupItemIconColor,
styleName: MobileAppBarComposerWidgetStyle.popupItemTextStyle,
padding: MobileAppBarComposerWidgetStyle.popupItemPadding,
onCallbackAction: () {
menuMoreOptionController.hideMenu();
saveToTemplateAction();
},
),
PopupItemWidget(
iconAction: imagePaths.icDeleteMailbox,
nameAction: AppLocalizations.of(context).delete,