TF-1961 Transform html email content in composer

(cherry picked from commit f5470c130744242195b265a0ea4aad044ad8e740)
This commit is contained in:
dab246
2023-08-18 19:42:04 +07:00
committed by Dat Vu
parent e06d35f1e1
commit 8eed49fd73
45 changed files with 1139 additions and 644 deletions
@@ -1,7 +1,9 @@
import 'dart:convert';
/// Transforms plain text messages.
abstract class TextTransformer {
const TextTransformer();
String process(String text);
String process(String text, HtmlEscape htmlEscape);
}
@@ -12,7 +12,9 @@ import 'package:html/dom.dart';
class ImageTransformer extends DomTransformer {
const ImageTransformer();
final bool useLoadingAttribute;
const ImageTransformer({this.useLoadingAttribute = false});
@override
Future<void> process({
@@ -40,14 +42,19 @@ class ImageTransformer extends DomTransformer {
);
imageElement.attributes['src'] = imageBase64 ?? src;
} else if (src.startsWith('https://') || src.startsWith('http://')) {
final classAttribute = imageElement.attributes['class'];
if (classAttribute != null) {
imageElement.attributes['class'] = '$classAttribute lazy-loading';
if (useLoadingAttribute) {
imageElement.attributes['loading'] = 'lazy';
} else {
imageElement.attributes['class'] = 'lazy-loading';
final classAttribute = imageElement.attributes['class'];
if (classAttribute != null) {
imageElement.attributes['class'] = '$classAttribute lazy-loading';
} else {
imageElement.attributes['class'] = 'lazy-loading';
}
imageElement.attributes['data-src'] = src;
imageElement.attributes.remove('src');
imageElement.attributes.remove('loading');
}
imageElement.attributes['data-src'] = src;
imageElement.attributes.remove('src');
}
}));
}
@@ -0,0 +1,35 @@
import 'package:core/data/network/dio_client.dart';
import 'package:core/presentation/utils/html_transformer/base/dom_transformer.dart';
import 'package:core/presentation/utils/html_transformer/html_template.dart';
import 'package:core/utils/app_logger.dart';
import 'package:html/dom.dart';
class RemoveTooltipLinkTransformer extends DomTransformer {
const RemoveTooltipLinkTransformer();
@override
Future<void> process({
required Document document,
Map<String, String>? mapUrlDownloadCID,
DioClient? dioClient
}) async {
final linkElements = document.querySelectorAll('a.$nameClassToolTip');
await Future.wait(linkElements.map((linkElement) async {
final classAttribute = linkElement.attributes['class'];
if (classAttribute != null) {
final newClassAttribute = classAttribute.replaceFirst(nameClassToolTip, '');
linkElement.attributes['class'] = newClassAttribute;
}
final listSpanTag = linkElement.querySelectorAll('span.tooltiptext');
log('RemoveTooltipLinkTransformer::process:listSpanTag: ${listSpanTag.length}');
if (listSpanTag.isNotEmpty) {
for (var element in listSpanTag) {
element.remove();
}
}
}));
}
}
@@ -0,0 +1,32 @@
import 'package:core/data/network/dio_client.dart';
import 'package:core/presentation/utils/html_transformer/base/dom_transformer.dart';
import 'package:html/dom.dart';
class ReplaceLazyLoadImageTransformer extends DomTransformer {
const ReplaceLazyLoadImageTransformer();
@override
Future<void> process({
required Document document,
Map<String, String>? mapUrlDownloadCID,
DioClient? dioClient
}) async {
final imageElements = document.querySelectorAll('img.lazy-loading');
await Future.wait(imageElements.map((imageElement) async {
final classAttribute = imageElement.attributes['class'];
if (classAttribute != null) {
final newClassAttribute = classAttribute.replaceFirst('lazy-loading', '');
imageElement.attributes['class'] = newClassAttribute;
}
final dataSrc = imageElement.attributes['data-src'];
if (dataSrc != null) {
imageElement.attributes['src'] = dataSrc;
imageElement.attributes.remove('data-src');
}
imageElement.attributes['loading'] = 'lazy';
}));
}
}
@@ -1,23 +1,26 @@
import 'dart:convert';
import 'package:core/core.dart';
import 'package:core/presentation/utils/html_transformer/message_content_transformer.dart';
class HtmlTransform {
final DioClient _dioClient;
final HtmlEscape _htmlEscape;
HtmlTransform(this._dioClient);
HtmlTransform(this._dioClient, this._htmlEscape);
/// Transforms this message to HTML code.
Future<String> transformToHtml({
required String contentHtml,
Map<String, String>? mapUrlDownloadCID,
required String htmlContent,
Map<String, String>? mapCidImageDownloadUrl,
TransformConfiguration? transformConfiguration,
}) async {
transformConfiguration ??= TransformConfiguration.create();
final transformer = MessageContentTransformer(transformConfiguration, _dioClient);
final transformer = MessageContentTransformer(transformConfiguration, _dioClient, _htmlEscape);
final document = await transformer.toDocument(
message: contentHtml,
mapUrlDownloadCID: mapUrlDownloadCID
message: htmlContent,
mapUrlDownloadCID: mapCidImageDownloadUrl
);
return document.outerHtml;
}
@@ -28,7 +31,7 @@ class HtmlTransform {
TransformConfiguration? transformConfiguration
}) {
transformConfiguration ??= TransformConfiguration.create();
final transformer = MessageContentTransformer(transformConfiguration, _dioClient);
final transformer = MessageContentTransformer(transformConfiguration, _dioClient, _htmlEscape);
final message = transformer.toMessage(content);
return message;
}
@@ -1,3 +1,5 @@
import 'dart:convert';
import 'package:core/data/network/dio_client.dart';
import 'package:core/presentation/utils/html_transformer/transform_configuration.dart';
import 'package:html/dom.dart';
@@ -5,23 +7,28 @@ import 'package:html/parser.dart' show parse;
/// Transforms messages
class MessageContentTransformer {
/// The configuration used for the transformation
final TransformConfiguration configuration;
final DioClient dioClient;
/// The _configuration used for the transformation
final TransformConfiguration _configuration;
final DioClient _dioClient;
final HtmlEscape _htmlEscape;
MessageContentTransformer(this.configuration, this.dioClient);
MessageContentTransformer(
this._configuration,
this._dioClient,
this._htmlEscape
);
Future<void> _transformDocument({
required Document document,
Map<String, String>? mapUrlDownloadCID
}) async {
await Future.wait([
if (configuration.domTransformers.isNotEmpty)
...configuration.domTransformers.map((domTransformer) async =>
if (_configuration.domTransformers.isNotEmpty)
..._configuration.domTransformers.map((domTransformer) async =>
domTransformer.process(
document: document,
mapUrlDownloadCID: mapUrlDownloadCID,
dioClient: dioClient
dioClient: _dioClient
)
)
]);
@@ -40,9 +47,9 @@ class MessageContentTransformer {
}
String _transformMessage(String message) {
if (configuration.textTransformers.isNotEmpty) {
for (var transformer in configuration.textTransformers) {
message = transformer.process(message);
if (_configuration.textTransformers.isNotEmpty) {
for (var transformer in _configuration.textTransformers) {
message = transformer.process(message, _htmlEscape);
}
}
return message;
@@ -6,10 +6,8 @@ import 'package:core/presentation/utils/html_transformer/base/text_transformer.d
class SanitizeAutolinkHtmlTransformers extends TextTransformer {
final HtmlEscape htmlEscape;
SanitizeAutolinkHtmlTransformers(this.htmlEscape);
const SanitizeAutolinkHtmlTransformers();
@override
String process(String text) => SanitizeAutolinkFilter(htmlEscape).process(text);
String process(String text, HtmlEscape htmlEscape) => SanitizeAutolinkFilter(htmlEscape).process(text);
}
@@ -1,12 +1,16 @@
import 'package:core/core.dart';
import 'package:core/presentation/utils/html_transformer/base/dom_transformer.dart';
import 'package:core/presentation/utils/html_transformer/base/text_transformer.dart';
import 'package:core/presentation/utils/html_transformer/dom/add_target_blank_in_tag_a_transformers.dart';
import 'package:core/presentation/utils/html_transformer/dom/blockcode_transformers.dart';
import 'package:core/presentation/utils/html_transformer/dom/blockquoted_transformers.dart';
import 'package:core/presentation/utils/html_transformer/dom/image_transformers.dart';
import 'package:core/presentation/utils/html_transformer/dom/remove_tooltip_link_transformers.dart';
import 'package:core/presentation/utils/html_transformer/dom/replace_lazy_load_image_transformer.dart';
import 'package:core/presentation/utils/html_transformer/dom/script_transformers.dart';
import 'package:core/presentation/utils/html_transformer/dom/sigature_transformers.dart';
import 'package:core/presentation/utils/html_transformer/text/sanitize_autolink_html_transformers.dart';
/// Contains the configuration for all transformations.
class TransformConfiguration {
@@ -25,6 +29,45 @@ class TransformConfiguration {
this.textTransformers
);
factory TransformConfiguration.forReplyForwardEmail() => TransformConfiguration.create(
customDomTransformers: [
const ReplaceLazyLoadImageTransformer(),
if (PlatformInfo.isWeb)
const RemoveTooltipLinkTransformer(),
]
);
factory TransformConfiguration.forDraftsEmail() => TransformConfiguration.create(
customDomTransformers: [
const RemoveScriptTransformer(),
const BlockQuotedTransformer(),
const BlockCodeTransformer(),
const AddTargetBlankInTagATransformer(),
const ImageTransformer(useLoadingAttribute: true),
]
);
factory TransformConfiguration.forComposeEmailPlatformWeb() => TransformConfiguration.create(
customDomTransformers: [
const RemoveScriptTransformer(),
const BlockQuotedTransformer(),
const BlockCodeTransformer(),
const AddTargetBlankInTagATransformer(),
const ImageTransformer(useLoadingAttribute: true)
]
);
factory TransformConfiguration.forPreviewEmailPlatformWeb() => TransformConfiguration.create(
customDomTransformers: [
const RemoveScriptTransformer(),
const BlockQuotedTransformer(),
const BlockCodeTransformer(),
const AddTargetBlankInTagATransformer(),
const ImageTransformer(),
const AddTooltipLinkTransformer(),
]
);
/// Provides easy access to a standard configuration that does not block external images.
static const TransformConfiguration standardConfiguration = TransformConfiguration(
standardDomTransformers,
@@ -61,13 +104,7 @@ class TransformConfiguration {
ImageTransformer(),
];
static const List<DomTransformer> domTransformersForDraftEmail = [
RemoveScriptTransformer(),
BlockQuotedTransformer(),
BlockCodeTransformer(),
AddTargetBlankInTagATransformer(),
ImageTransformer(),
static const List<TextTransformer> standardTextTransformers = [
SanitizeAutolinkHtmlTransformers()
];
static const List<TextTransformer> standardTextTransformers = [];
}
@@ -24,7 +24,7 @@ import 'package:tmail_ui_user/features/session/domain/usecases/get_session_inter
import 'package:tmail_ui_user/main/error/capability_validator.dart';
abstract class ReloadableController extends BaseController {
final DynamicUrlInterceptors _dynamicUrlInterceptors = Get.find<DynamicUrlInterceptors>();
final DynamicUrlInterceptors dynamicUrlInterceptors = Get.find<DynamicUrlInterceptors>();
final GetSessionInteractor _getSessionInteractor = Get.find<GetSessionInteractor>();
final GetAuthenticatedAccountInteractor _getAuthenticatedAccountInteractor;
final UpdateAuthenticationAccountInteractor _updateAuthenticationAccountInteractor;
@@ -72,8 +72,8 @@ abstract class ReloadableController extends BaseController {
}
void _setUpInterceptors(GetCredentialViewState credentialViewState) {
_dynamicUrlInterceptors.setJmapUrl(credentialViewState.baseUrl.origin);
_dynamicUrlInterceptors.changeBaseUrl(credentialViewState.baseUrl.origin);
dynamicUrlInterceptors.setJmapUrl(credentialViewState.baseUrl.origin);
dynamicUrlInterceptors.changeBaseUrl(credentialViewState.baseUrl.origin);
authorizationInterceptors.setBasicAuthorization(
credentialViewState.userName.value,
credentialViewState.password.value,
@@ -100,10 +100,10 @@ abstract class ReloadableController extends BaseController {
void _handleGetSessionSuccess(GetSessionSuccess success) {
final session = success.session;
final personalAccount = session.personalAccount;
final apiUrl = session.getQualifiedApiUrl(baseUrl: _dynamicUrlInterceptors.jmapUrl);
final apiUrl = session.getQualifiedApiUrl(baseUrl: dynamicUrlInterceptors.jmapUrl);
log('ReloadableController::_handleGetSessionSuccess():apiUrl: $apiUrl');
if (apiUrl.isNotEmpty) {
_dynamicUrlInterceptors.changeBaseUrl(apiUrl);
dynamicUrlInterceptors.changeBaseUrl(apiUrl);
updateAuthenticationAccount(session, personalAccount.accountId, session.username);
handleReloaded(session);
} else {
@@ -120,8 +120,8 @@ abstract class ReloadableController extends BaseController {
}
void _setUpInterceptorsOidc(GetStoredTokenOidcSuccess tokenOidcSuccess) {
_dynamicUrlInterceptors.setJmapUrl(tokenOidcSuccess.baseUrl.toString());
_dynamicUrlInterceptors.changeBaseUrl(tokenOidcSuccess.baseUrl.toString());
dynamicUrlInterceptors.setJmapUrl(tokenOidcSuccess.baseUrl.toString());
dynamicUrlInterceptors.changeBaseUrl(tokenOidcSuccess.baseUrl.toString());
authorizationInterceptors.setTokenAndAuthorityOidc(
newToken: tokenOidcSuccess.tokenOidc.toToken(),
newConfig: tokenOidcSuccess.oidcConfiguration);
@@ -140,7 +140,7 @@ abstract class ReloadableController extends BaseController {
}
void updateAuthenticationAccount(Session session, AccountId accountId, UserName userName) {
final apiUrl = session.getQualifiedApiUrl(baseUrl: _dynamicUrlInterceptors.jmapUrl);
final apiUrl = session.getQualifiedApiUrl(baseUrl: dynamicUrlInterceptors.jmapUrl);
log('ReloadableController::updateAuthenticationAccount():apiUrl: $apiUrl');
if (apiUrl.isNotEmpty) {
consumeState(_updateAuthenticationAccountInteractor.execute(accountId, apiUrl, userName));
@@ -4,17 +4,35 @@ import 'package:tmail_ui_user/features/base/styles/cupertino_loading_widget_styl
class CupertinoLoadingWidget extends StatelessWidget {
final double? size;
final EdgeInsetsGeometry? padding;
const CupertinoLoadingWidget({super.key, this.size});
const CupertinoLoadingWidget({super.key, this.size, this.padding});
@override
Widget build(BuildContext context) {
return Center(
child: SizedBox(
width: size ?? CupertinoLoadingWidgetStyles.size,
height: size ?? CupertinoLoadingWidgetStyles.size,
child: const CupertinoActivityIndicator(color: CupertinoLoadingWidgetStyles.progressColor)
)
);
if (padding != null) {
return Padding(
padding: padding!,
child: Center(
child: SizedBox(
width: size ?? CupertinoLoadingWidgetStyles.size,
height: size ?? CupertinoLoadingWidgetStyles.size,
child: const CupertinoActivityIndicator(
color: CupertinoLoadingWidgetStyles.progressColor
)
)
),
);
} else {
return Center(
child: SizedBox(
width: size ?? CupertinoLoadingWidgetStyles.size,
height: size ?? CupertinoLoadingWidgetStyles.size,
child: const CupertinoActivityIndicator(
color: CupertinoLoadingWidgetStyles.progressColor
)
)
);
}
}
}
@@ -5,6 +5,7 @@ import 'package:core/presentation/utils/app_toast.dart';
import 'package:core/presentation/utils/responsive_utils.dart';
import 'package:core/presentation/utils/style_utils.dart';
import 'package:core/presentation/views/button/icon_button_web.dart';
import 'package:core/presentation/views/button/tmail_button_widget.dart';
import 'package:core/presentation/views/list/sliver_grid_delegate_fixed_height.dart';
import 'package:core/presentation/views/text/text_field_builder.dart';
import 'package:core/presentation/views/text/text_overflow_builder.dart';
@@ -321,22 +322,17 @@ abstract class BaseComposerView extends GetWidget<ComposerController>
)
),
const Spacer(),
Material(
type: MaterialType.circle,
color: Colors.transparent,
child: TextButton(
onPressed: controller.toggleDisplayAttachments,
child: Text(
expandModeAttachment == ExpandMode.EXPAND
? AppLocalizations.of(context).hide
: '${AppLocalizations.of(context).showAll} (${uploadFilesState.length})',
style: const TextStyle(
fontWeight: FontWeight.w500,
fontSize: 12,
color: AppColor.colorTextButton
)
)
)
TMailButtonWidget.fromText(
text: expandModeAttachment == ExpandMode.EXPAND
? AppLocalizations.of(context).hide
: '${AppLocalizations.of(context).showAll} (${uploadFilesState.length})',
onTapActionCallback: controller.toggleDisplayAttachments,
textStyle: const TextStyle(
fontWeight: FontWeight.w500,
fontSize: 12,
color: AppColor.colorTextButton
),
backgroundColor: Colors.transparent,
)
],
);
@@ -29,6 +29,7 @@ import 'package:tmail_ui_user/features/email/data/network/email_api.dart';
import 'package:tmail_ui_user/features/email/data/repository/email_repository_impl.dart';
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/transform_html_email_content_interactor.dart';
import 'package:tmail_ui_user/features/mailbox/data/datasource/mailbox_datasource.dart';
import 'package:tmail_ui_user/features/mailbox/data/datasource/state_datasource.dart';
import 'package:tmail_ui_user/features/mailbox/data/datasource_impl/mailbox_cache_datasource_impl.dart';
@@ -163,6 +164,7 @@ class ComposerBindings extends BaseBindings {
Get.lazyPut(() => RemoveComposerCacheOnWebInteractor(Get.find<ComposerCacheRepository>()));
Get.lazyPut(() => SaveComposerCacheOnWebInteractor(Get.find<ComposerCacheRepository>()));
Get.lazyPut(() => DownloadImageAsBase64Interactor(Get.find<ComposerRepository>()));
Get.lazyPut(() => TransformHtmlEmailContentInteractor(Get.find<EmailRepository>()));
IdentityInteractorsBindings().dependencies();
}
@@ -184,6 +186,7 @@ class ComposerBindings extends BaseBindings {
Get.find<SaveComposerCacheOnWebInteractor>(),
Get.find<RichTextWebController>(),
Get.find<DownloadImageAsBase64Interactor>(),
Get.find<TransformHtmlEmailContentInteractor>(),
));
}
@@ -48,8 +48,11 @@ import 'package:tmail_ui_user/features/composer/presentation/extensions/email_ac
import 'package:tmail_ui_user/features/composer/presentation/model/image_source.dart';
import 'package:tmail_ui_user/features/composer/presentation/model/inline_image.dart';
import 'package:tmail_ui_user/features/composer/presentation/model/screen_display_mode.dart';
import 'package:tmail_ui_user/features/email/domain/exceptions/email_exceptions.dart';
import 'package:tmail_ui_user/features/email/domain/state/get_email_content_state.dart';
import 'package:tmail_ui_user/features/email/domain/state/transform_html_email_content_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/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/domain/model/create_new_mailbox_request.dart';
import 'package:tmail_ui_user/features/mailbox_dashboard/domain/usecases/remove_composer_cache_on_web_interactor.dart';
@@ -60,6 +63,7 @@ import 'package:tmail_ui_user/features/manage_account/domain/usecases/get_all_id
import 'package:tmail_ui_user/features/manage_account/presentation/extensions/identity_extension.dart';
import 'package:tmail_ui_user/features/network_connection/presentation/network_connection_controller.dart';
import 'package:tmail_ui_user/features/sending_queue/domain/extensions/sending_email_extension.dart';
import 'package:tmail_ui_user/features/sending_queue/domain/model/sending_email.dart';
import 'package:tmail_ui_user/features/sending_queue/presentation/model/sending_email_action_type.dart';
import 'package:tmail_ui_user/features/sending_queue/presentation/model/sending_email_arguments.dart';
import 'package:tmail_ui_user/features/upload/domain/model/upload_task_id.dart';
@@ -95,7 +99,7 @@ class ComposerController extends BaseController {
final bccAddressExpandMode = ExpandMode.EXPAND.obs;
final identitySelected = Rxn<Identity>();
final listIdentities = <Identity>[].obs;
final emailContentsViewState = Rx<Either<Failure, Success>>(Right(UIState.idle));
final emailContentsViewState = Rxn<Either<Failure, Success>>();
final hasRequestReadReceipt = false.obs;
final LocalFilePickerInteractor _localFilePickerInteractor;
@@ -109,6 +113,7 @@ class ComposerController extends BaseController {
final SaveComposerCacheOnWebInteractor _saveComposerCacheOnWebInteractor;
final RichTextWebController richTextWebController;
final DownloadImageAsBase64Interactor _downloadImageAsBase64Interactor;
final TransformHtmlEmailContentInteractor _transformHtmlEmailContentInteractor;
GetAutoCompleteWithDeviceContactInteractor? _getAutoCompleteWithDeviceContactInteractor;
GetAutoCompleteInteractor? _getAutoCompleteInteractor;
@@ -145,7 +150,6 @@ class ComposerController extends BaseController {
List<Attachment> initialAttachments = <Attachment>[];
String? _textEditorWeb;
String? _initTextEditor;
String? _emailContents;
double? maxWithEditor;
late Worker uploadInlineImageWorker;
@@ -201,6 +205,7 @@ class ComposerController extends BaseController {
this._saveComposerCacheOnWebInteractor,
this.richTextWebController,
this._downloadImageAsBase64Interactor,
this._transformHtmlEmailContentInteractor,
);
@override
@@ -264,7 +269,9 @@ class ComposerController extends BaseController {
@override
void handleSuccessViewState(Success success) {
super.handleSuccessViewState(success);
if (success is GetEmailContentLoading) {
if (success is GetEmailContentLoading ||
success is TransformHtmlEmailContentLoading ||
success is TransformHtmlEmailContentSuccess) {
emailContentsViewState.value = Right(success);
} else if (success is LocalFilePickerSuccess) {
_pickFileSuccess(success);
@@ -299,7 +306,8 @@ class ComposerController extends BaseController {
super.handleFailureViewState(failure);
if (failure is LocalFilePickerFailure || failure is LocalFilePickerCancel) {
_pickFileFailure(failure);
} else if (failure is GetEmailContentFailure) {
} else if (failure is GetEmailContentFailure ||
failure is TransformHtmlEmailContentFailure) {
emailContentsViewState.value = Left(failure);
}
}
@@ -387,39 +395,105 @@ class ComposerController extends BaseController {
: Get.arguments;
if (arguments is ComposerArguments) {
composerArguments.value = arguments;
injectAutoCompleteBindings(
mailboxDashBoardController.sessionCurrent,
mailboxDashBoardController.accountId.value);
mailboxDashBoardController.sessionCurrent,
mailboxDashBoardController.accountId.value
);
if (arguments.emailActionType == EmailActionType.edit) {
_getEmailContentAction(arguments);
switch(arguments.emailActionType) {
case EmailActionType.editDraft:
_initEmailAddress(
presentationEmail: arguments.presentationEmail!,
actionType: EmailActionType.editDraft
);
_initSubjectEmail(
presentationEmail: arguments.presentationEmail!,
actionType: EmailActionType.editDraft
);
_getEmailContentFromEmailId(
emailId: arguments.presentationEmail!.id!,
isDraftEmail: arguments.presentationEmail!.isDraft
);
break;
case EmailActionType.editSendingEmail:
_initEmailAddress(
presentationEmail: arguments.sendingEmail!.presentationEmail,
actionType: EmailActionType.editSendingEmail
);
_initSubjectEmail(
presentationEmail: arguments.sendingEmail!.presentationEmail,
actionType: EmailActionType.editSendingEmail
);
_getEmailContentFromSendingEmail(arguments.sendingEmail!);
break;
case EmailActionType.composeFromContentShared:
_getEmailContentFromContentShared(arguments.emailContents!);
break;
case EmailActionType.composeFromFileShared:
_addAttachmentFromFileShare(arguments.listSharedMediaFile!);
break;
case EmailActionType.composeFromEmailAddress:
listToEmailAddress.add(arguments.emailAddress!);
isInitialRecipient.value = true;
toAddressExpandMode.value = ExpandMode.COLLAPSE;
_updateStatusEmailSendButton();
break;
case EmailActionType.reply:
case EmailActionType.replyAll:
_initEmailAddress(
presentationEmail: arguments.presentationEmail!,
actionType: arguments.emailActionType,
mailboxRole: arguments.presentationEmail!.mailboxContain?.role ?? mailboxDashBoardController.selectedMailbox.value?.role
);
_initSubjectEmail(
presentationEmail: arguments.presentationEmail!,
actionType: arguments.emailActionType
);
_transformHtmlEmailContent(arguments.emailContents);
break;
case EmailActionType.forward:
_initSubjectEmail(
presentationEmail: arguments.presentationEmail!,
actionType: arguments.emailActionType
);
_initAttachments(arguments.attachments ?? []);
_transformHtmlEmailContent(arguments.emailContents);
break;
case EmailActionType.reopenComposerBrowser:
_initEmailAddress(
presentationEmail: arguments.presentationEmail!,
actionType: EmailActionType.reopenComposerBrowser
);
_initSubjectEmail(
presentationEmail: arguments.presentationEmail!,
actionType: EmailActionType.reopenComposerBrowser
);
_initAttachments(arguments.attachments ?? []);
_getEmailContentFromSessionStorageBrowser(arguments.emailContents!);
break;
default:
break;
}
_initEmailAddress(arguments);
_initSubjectEmail(arguments);
_initAttachments(arguments);
}
_autoFocusFieldWhenLauncher();
}
void _initSubjectEmail(ComposerArguments arguments) {
if (currentContext != null) {
final subjectEmail = arguments.presentationEmail?.getEmailTitle().trim() ?? '';
final newSubject = arguments.emailActionType.getSubjectComposer(currentContext!, subjectEmail);
setSubjectEmail(newSubject);
subjectEmailInputController.text = newSubject;
}
void _initSubjectEmail({
required PresentationEmail presentationEmail,
required EmailActionType actionType
}) {
final subjectEmail = presentationEmail.getEmailTitle().trim();
final newSubject = actionType.getSubjectComposer(currentContext, subjectEmail);
setSubjectEmail(newSubject);
subjectEmailInputController.text = newSubject;
}
void _initAttachments(ComposerArguments arguments) {
if (arguments.attachments?.isNotEmpty == true) {
initialAttachments = arguments.attachments!;
uploadController.initializeUploadAttachments(
arguments.attachments!.listAttachmentsDisplayedOutSide);
}
if (PlatformInfo.isWeb) {
expandModeAttachments.value = ExpandMode.EXPAND;
void _initAttachments(List<Attachment> attachments) {
if (attachments.isNotEmpty) {
initialAttachments = attachments;
uploadController.initializeUploadAttachments(attachments.listAttachmentsDisplayedOutSide);
}
}
@@ -438,7 +512,6 @@ class ComposerController extends BaseController {
.toList();
if (listIdentities.isNotEmpty) {
_initTextEditor = null;
await selectIdentity(listIdentities.first);
}
}
@@ -446,113 +519,49 @@ class ComposerController extends BaseController {
_autoFocusFieldWhenLauncher();
}
String? _getHeaderEmailQuoted(BuildContext context, ComposerArguments arguments) {
final presentationEmail = arguments.presentationEmail;
if (presentationEmail != null) {
final locale = Localizations.localeOf(context).toLanguageTag();
log('ComposerController::_getHeaderEmailQuoted(): emailActionType: ${arguments.emailActionType}');
switch(arguments.emailActionType) {
case EmailActionType.reply:
case EmailActionType.replyAll:
final receivedAt = presentationEmail.receivedAt;
final emailAddress = presentationEmail.from.listEmailAddressToString(isFullEmailAddress: true);
return AppLocalizations.of(context).header_email_quoted(
receivedAt.formatDateToLocal(pattern: 'MMM d, y h:mm a', locale: locale),
emailAddress);
case EmailActionType.forward:
var headerQuoted = '------- ${AppLocalizations.of(context).forwarded_message} -------'.addNewLineTag();
final subject = presentationEmail.subject ?? '';
final receivedAt = presentationEmail.receivedAt;
final fromEmailAddress = presentationEmail.from.listEmailAddressToString(isFullEmailAddress: true);
final toEmailAddress = presentationEmail.to.listEmailAddressToString(isFullEmailAddress: true);
final ccEmailAddress = presentationEmail.cc.listEmailAddressToString(isFullEmailAddress: true);
final bccEmailAddress = presentationEmail.bcc.listEmailAddressToString(isFullEmailAddress: true);
if (subject.isNotEmpty) {
headerQuoted = headerQuoted
.append('${AppLocalizations.of(context).subject_email}: ')
.append(subject)
.addNewLineTag();
}
if (receivedAt != null) {
headerQuoted = headerQuoted
.append('${AppLocalizations.of(context).date}: ')
.append(receivedAt.formatDateToLocal(pattern: 'MMM d, y h:mm a', locale: locale))
.addNewLineTag();
}
if (fromEmailAddress.isNotEmpty) {
headerQuoted = headerQuoted
.append('${AppLocalizations.of(context).from_email_address_prefix}: ')
.append(fromEmailAddress)
.addNewLineTag();
}
if (toEmailAddress.isNotEmpty) {
headerQuoted = headerQuoted
.append('${AppLocalizations.of(context).to_email_address_prefix}: ')
.append(toEmailAddress)
.addNewLineTag();
}
if (ccEmailAddress.isNotEmpty) {
headerQuoted = headerQuoted
.append('${AppLocalizations.of(context).cc_email_address_prefix}: ')
.append(ccEmailAddress)
.addNewLineTag();
}
if (bccEmailAddress.isNotEmpty) {
headerQuoted = headerQuoted
.append('${AppLocalizations.of(context).bcc_email_address_prefix}: ')
.append(bccEmailAddress)
.addNewLineTag();
}
return headerQuoted;
default:
return null;
}
}
return null;
}
void _initEmailAddress(ComposerArguments arguments) {
void _initEmailAddress({
required PresentationEmail presentationEmail,
required EmailActionType actionType,
Role? mailboxRole,
}) {
final recipients = presentationEmail.generateRecipientsEmailAddressForComposer(
emailActionType: actionType,
mailboxRole: mailboxRole
);
log('ComposerController::_initEmailAddress:recipients: $recipients');
final userProfile = mailboxDashBoardController.userProfile.value;
if (arguments.presentationEmail != null && userProfile != null) {
final userEmailAddress = EmailAddress(null, userProfile.email);
final recipients = arguments.presentationEmail!.generateRecipientsEmailAddressForComposer(
arguments.emailActionType,
arguments.mailboxRole);
final isSender = arguments.presentationEmail!.from.asList().every((element) => element.email == userEmailAddress.email);
if (userProfile != null) {
final isSender = presentationEmail.from.asList().every((element) => element.email == userProfile.email);
if (isSender) {
listToEmailAddress = List.from(recipients.value1);
listCcEmailAddress = List.from(recipients.value2);
listBccEmailAddress = List.from(recipients.value3);
listToEmailAddress = List.from(recipients.value1.toSet());
listCcEmailAddress = List.from(recipients.value2.toSet());
listBccEmailAddress = List.from(recipients.value3.toSet());
} else {
listToEmailAddress = List.from(recipients.value1.toSet().filterEmailAddress(userEmailAddress));
listCcEmailAddress = List.from(recipients.value2.toSet().filterEmailAddress(userEmailAddress));
listBccEmailAddress = List.from(recipients.value3.toSet().filterEmailAddress(userEmailAddress));
listToEmailAddress = List.from(recipients.value1.toSet().filterEmailAddress(userProfile.email));
listCcEmailAddress = List.from(recipients.value2.toSet().filterEmailAddress(userProfile.email));
listBccEmailAddress = List.from(recipients.value3.toSet().filterEmailAddress(userProfile.email));
}
} else {
listToEmailAddress = List.from(recipients.value1.toSet());
listCcEmailAddress = List.from(recipients.value2.toSet());
listBccEmailAddress = List.from(recipients.value3.toSet());
}
if (listToEmailAddress.isNotEmpty || listCcEmailAddress.isNotEmpty || listBccEmailAddress.isNotEmpty) {
isInitialRecipient.value = true;
toAddressExpandMode.value = ExpandMode.COLLAPSE;
}
if (listCcEmailAddress.isNotEmpty) {
listEmailAddressType.add(PrefixEmailAddress.cc);
ccAddressExpandMode.value = ExpandMode.COLLAPSE;
}
if (listBccEmailAddress.isNotEmpty) {
listEmailAddressType.add(PrefixEmailAddress.bcc);
bccAddressExpandMode.value = ExpandMode.COLLAPSE;
}
} else if (arguments.emailAddress != null) {
listToEmailAddress.add(arguments.emailAddress!);
if (listToEmailAddress.isNotEmpty || listCcEmailAddress.isNotEmpty || listBccEmailAddress.isNotEmpty) {
isInitialRecipient.value = true;
toAddressExpandMode.value = ExpandMode.COLLAPSE;
}
if (listCcEmailAddress.isNotEmpty) {
listEmailAddressType.add(PrefixEmailAddress.cc);
ccAddressExpandMode.value = ExpandMode.COLLAPSE;
}
if (listBccEmailAddress.isNotEmpty) {
listEmailAddressType.add(PrefixEmailAddress.bcc);
bccAddressExpandMode.value = ExpandMode.COLLAPSE;
}
_updateStatusEmailSendButton();
}
@@ -586,13 +595,21 @@ class ComposerController extends BaseController {
}
}
String getEmailContentQuotedAsHtml(BuildContext context, ComposerArguments arguments) {
final headerEmailQuoted = _getHeaderEmailQuoted(context, arguments);
String getEmailContentQuotedAsHtml({
required BuildContext context,
required String emailContent,
required EmailActionType emailActionType,
required PresentationEmail presentationEmail,
}) {
final headerEmailQuoted = emailActionType.getHeaderEmailQuoted(
context: context,
presentationEmail: presentationEmail
);
log('ComposerController::getEmailContentQuotedAsHtml(): headerEmailQuoted: $headerEmailQuoted');
final headerEmailQuotedAsHtml = headerEmailQuoted != null ? headerEmailQuoted.addBlockTag('cite') : '';
final trustAsHtml = arguments.emailContents ?? '';
final emailQuotedHtml = '${HtmlExtension.editorStartTags}$headerEmailQuotedAsHtml${trustAsHtml.addBlockQuoteTag()}';
final headerEmailQuotedAsHtml = headerEmailQuoted != null
? headerEmailQuoted.addCiteTag()
: '';
final emailQuotedHtml = '${HtmlExtension.editorStartTags}$headerEmailQuotedAsHtml${emailContent.addBlockQuoteTag()}';
return emailQuotedHtml;
}
@@ -789,13 +806,13 @@ class ComposerController extends BaseController {
if (arguments != null && accountId != null && userProfile != null && session != null) {
final createdEmail = await _generateEmail(context, userProfile, outboxMailboxId: outboxMailboxId);
final emailRequest = arguments.sendingEmail != null
final emailRequest = arguments.emailActionType == EmailActionType.editSendingEmail
? arguments.sendingEmail!.toEmailRequest(newEmail: createdEmail)
: EmailRequest(
email: createdEmail,
sentMailboxId: sentMailboxId,
identityId: identitySelected.value?.id,
emailIdDestroyed: arguments.emailActionType == EmailActionType.edit
emailIdDestroyed: arguments.emailActionType == EmailActionType.editDraft
? arguments.presentationEmail?.id
: null,
emailIdAnsweredOrForwarded: arguments.presentationEmail?.id,
@@ -952,36 +969,40 @@ class ComposerController extends BaseController {
uploadController.deleteFileUploaded(uploadId);
}
Future<bool> _isEmailChanged(
BuildContext context,
ComposerArguments arguments,
) async {
Future<bool> _isEmailChanged({
required BuildContext context,
required EmailActionType emailActionType,
PresentationEmail? presentationEmail,
Role? mailboxRole,
}) async {
final newEmailBody = await _getEmailBodyText(context, changedEmail: true);
log('ComposerController::_isEmailChanged(): newEmailBody: $newEmailBody');
var oldEmailBody = _initTextEditor ?? '';
final oldEmailBody = _initTextEditor ?? '';
log('ComposerController::_isEmailChanged(): oldEmailBody: $oldEmailBody');
final isEmailBodyChanged = !oldEmailBody.trim().isSame(newEmailBody.trim());
log('ComposerController::_isEmailChanged(): isEmailBodyChanged: $isEmailBodyChanged');
final newEmailSubject = subjectEmail.value ?? '';
final titleEmail = arguments.presentationEmail?.getEmailTitle().trim() ?? '';
final oldEmailSubject = arguments.emailActionType == EmailActionType.edit ? titleEmail : '';
final oldEmailSubject = emailActionType == EmailActionType.editDraft
? presentationEmail?.getEmailTitle().trim() ?? ''
: '';
final isEmailSubjectChanged = !oldEmailSubject.trim().isSame(newEmailSubject.trim());
final recipients = arguments.presentationEmail
?.generateRecipientsEmailAddressForComposer(arguments.emailActionType, arguments.mailboxRole)
?? const Tuple3(<EmailAddress>[], <EmailAddress>[], <EmailAddress>[]);
final recipients = presentationEmail
?.generateRecipientsEmailAddressForComposer(
emailActionType: emailActionType,
mailboxRole: mailboxRole
) ?? const Tuple3(<EmailAddress>[], <EmailAddress>[], <EmailAddress>[]);
final newToEmailAddress = listToEmailAddress;
final oldToEmailAddress = arguments.emailActionType == EmailActionType.edit ? recipients.value1 : [];
final oldToEmailAddress = emailActionType == EmailActionType.editDraft ? recipients.value1 : [];
final isToEmailAddressChanged = !oldToEmailAddress.isSame(newToEmailAddress);
final newCcEmailAddress = listCcEmailAddress;
final oldCcEmailAddress = arguments.emailActionType == EmailActionType.edit ? recipients.value2 : [];
final oldCcEmailAddress = emailActionType == EmailActionType.editDraft ? recipients.value2 : [];
final isCcEmailAddressChanged = !oldCcEmailAddress.isSame(newCcEmailAddress);
final newBccEmailAddress = listBccEmailAddress;
final oldBccEmailAddress = arguments.emailActionType == EmailActionType.edit ? recipients.value3 : [];
final oldBccEmailAddress = emailActionType == EmailActionType.editDraft ? recipients.value3 : [];
final isBccEmailAddressChanged = !oldBccEmailAddress.isSame(newBccEmailAddress);
final isAttachmentsChanged = !initialAttachments.isSame(uploadController.attachmentsUploaded.toList());
@@ -999,36 +1020,60 @@ class ComposerController extends BaseController {
clearFocusEditor(context);
final arguments = composerArguments.value;
final draftMailboxId = mailboxDashBoardController.mapDefaultMailboxIdByRole[PresentationMailbox.roleDrafts];
final userProfile = mailboxDashBoardController.userProfile.value;
final accountId = mailboxDashBoardController.accountId.value;
final session = mailboxDashBoardController.sessionCurrent;
if (arguments != null && userProfile != null && accountId != null && session != null) {
final isChanged = await _isEmailChanged(context, arguments);
if (isChanged && context.mounted) {
final newEmail = await _generateEmail(
context,
userProfile,
asDrafts: true,
draftMailboxId: draftMailboxId);
final oldEmail = arguments.presentationEmail;
if (arguments.emailActionType == EmailActionType.edit && oldEmail != null && oldEmail.id != null) {
mailboxDashBoardController.consumeState(
_updateEmailDraftsInteractor.execute(
session,
accountId,
newEmail,
oldEmail.id!
)
);
} else {
mailboxDashBoardController.consumeState(_saveEmailAsDraftsInteractor.execute(session, accountId, newEmail));
}
uploadController.clearInlineFileUploaded();
if (arguments == null ||
userProfile == null ||
session == null ||
accountId == null
) {
if (PlatformInfo.isWeb) {
mailboxDashBoardController.closeComposerOverlay();
} else {
if (canPop) popBack();
}
return;
}
final isChanged = await _isEmailChanged(
context: context,
emailActionType: arguments.emailActionType,
presentationEmail: arguments.presentationEmail,
mailboxRole: arguments.mailboxRole
);
if (isChanged && context.mounted) {
final draftMailboxId = mailboxDashBoardController.mapDefaultMailboxIdByRole[PresentationMailbox.roleDrafts];
final newEmail = await _generateEmail(
context,
userProfile,
asDrafts: true,
draftMailboxId: draftMailboxId
);
if (arguments.emailActionType == EmailActionType.editDraft) {
mailboxDashBoardController.consumeState(
_updateEmailDraftsInteractor.execute(
session,
accountId,
newEmail,
arguments.presentationEmail!.id!
)
);
} else {
mailboxDashBoardController.consumeState(
_saveEmailAsDraftsInteractor.execute(
session,
accountId,
newEmail
)
);
}
uploadController.clearInlineFileUploaded();
}
if (PlatformInfo.isWeb) {
@@ -1087,90 +1132,109 @@ class ComposerController extends BaseController {
return listFileInfo;
}
void _getEmailContentAction(ComposerArguments arguments) async {
final listSharedMediaFile = arguments.listSharedMediaFile;
if (listSharedMediaFile != null && listSharedMediaFile.isNotEmpty) {
final listImageSharedMediaFile = listSharedMediaFile.where((element) => element.type == SharedMediaType.IMAGE);
final listFileAttachmentSharedMediaFile = listSharedMediaFile.where((element) => element.type != SharedMediaType.IMAGE);
if (listImageSharedMediaFile.isNotEmpty) {
final listInlineImage = covertListSharedMediaFileToInlineImage(arguments.listSharedMediaFile!);
for (var e in listInlineImage) {
_uploadInlineAttachmentsAction(e.fileInfo!);
}
void _addAttachmentFromFileShare(List<SharedMediaFile> listSharedMediaFile) {
final listImageSharedMediaFile = listSharedMediaFile.where((element) => element.type == SharedMediaType.IMAGE);
final listFileAttachmentSharedMediaFile = listSharedMediaFile.where((element) => element.type != SharedMediaType.IMAGE);
if (listImageSharedMediaFile.isNotEmpty) {
final listInlineImage = covertListSharedMediaFileToInlineImage(listSharedMediaFile);
for (var e in listInlineImage) {
_uploadInlineAttachmentsAction(e.fileInfo!);
}
if (listFileAttachmentSharedMediaFile.isNotEmpty) {
final listFile = covertListSharedMediaFileToFileInfo(arguments.listSharedMediaFile!);
if (uploadController.hasEnoughMaxAttachmentSize(listFiles: listFile)) {
_uploadAttachmentsAction(listFile);
} else {
if (currentContext != null) {
showConfirmDialogAction(
currentContext!,
AppLocalizations.of(currentContext!).message_dialog_upload_attachments_exceeds_maximum_size(
filesize(mailboxDashBoardController.maxSizeAttachmentsPerEmail?.value ?? 0, 0)),
AppLocalizations.of(currentContext!).got_it,
onConfirmAction: () => {},
title: AppLocalizations.of(currentContext!).maximum_files_size,
hasCancelButton: false,
);
}
}
if (listFileAttachmentSharedMediaFile.isNotEmpty) {
final listFile = covertListSharedMediaFileToFileInfo(listSharedMediaFile);
if (uploadController.hasEnoughMaxAttachmentSize(listFiles: listFile)) {
_uploadAttachmentsAction(listFile);
} else {
if (currentContext != null) {
showConfirmDialogAction(
currentContext!,
AppLocalizations.of(currentContext!).message_dialog_upload_attachments_exceeds_maximum_size(
filesize(mailboxDashBoardController.maxSizeAttachmentsPerEmail?.value ?? 0, 0)),
AppLocalizations.of(currentContext!).got_it,
title: AppLocalizations.of(currentContext!).maximum_files_size,
hasCancelButton: false,
);
}
}
}
}
if (arguments.emailContents != null && arguments.emailContents!.isNotEmpty && arguments.sendingEmail != null) {
_emailContents = arguments.emailContents;
emailContentsViewState.value = Right(
GetEmailContentSuccess(
emailContent: _emailContents!,
emailContentDisplayed: '',
attachments: [],
emailCurrent: arguments.presentationEmail?.toEmail()
)
);
} else {
final session = mailboxDashBoardController.sessionCurrent;
final baseDownloadUrl = mailboxDashBoardController.sessionCurrent?.getDownloadUrl(jmapUrl: _dynamicUrlInterceptors.jmapUrl);
final accountId = mailboxDashBoardController.sessionCurrent?.accounts.keys.first;
final emailId = arguments.presentationEmail?.id;
if (session != null && emailId != null && baseDownloadUrl != null && accountId != null) {
consumeState(_getEmailContentInteractor.execute(
session,
accountId,
emailId,
baseDownloadUrl,
composeEmail: true,
draftsEmail: arguments.presentationEmail?.isDraft ?? false
));
void _getEmailContentFromSendingEmail(SendingEmail sendingEmail) {
consumeState(Stream.value(
Right(GetEmailContentSuccess(
htmlEmailContent: sendingEmail.presentationEmail.emailContentList.asHtmlString,
attachments: sendingEmail.email.allAttachments,
emailCurrent: sendingEmail.email
))
));
}
void _getEmailContentFromSessionStorageBrowser(String content) {
consumeState(Stream.value(
Right(GetEmailContentSuccess(
htmlEmailContent: content,
attachments: [],
))
));
}
void _getEmailContentFromContentShared(String content) {
consumeState(Stream.value(
Right(GetEmailContentSuccess(
htmlEmailContent: content,
attachments: [],
))
));
}
void _getEmailContentFromEmailId({required EmailId emailId, bool isDraftEmail = false}) {
final session = mailboxDashBoardController.sessionCurrent;
final accountId = mailboxDashBoardController.accountId.value;
if (session != null && accountId != null) {
TransformConfiguration transformConfiguration = TransformConfiguration.standardConfiguration;
if (isDraftEmail) {
transformConfiguration = TransformConfiguration.forDraftsEmail();
} else if (PlatformInfo.isWeb) {
transformConfiguration = TransformConfiguration.forComposeEmailPlatformWeb();
}
consumeState(_getEmailContentInteractor.execute(
session,
accountId,
emailId,
mailboxDashBoardController.baseDownloadUrl,
transformConfiguration
));
}
}
void _getEmailContentOffLineSuccess(GetEmailContentFromCacheSuccess success) {
if (success.attachments.isNotEmpty) {
initialAttachments = success.attachments;
uploadController.initializeUploadAttachments(
success.attachments.listAttachmentsDisplayedOutSide);
}
_initAttachments(success.attachments);
emailContentsViewState.value = Right(success);
_emailContents = success.emailContent;
}
void _getEmailContentSuccess(GetEmailContentSuccess success) {
if (success.attachments.isNotEmpty) {
initialAttachments = success.attachments;
uploadController.initializeUploadAttachments(
success.attachments.listAttachmentsDisplayedOutSide);
}
_initAttachments(success.attachments);
emailContentsViewState.value = Right(success);
_emailContents = success.emailContent;
}
void _transformHtmlEmailContent(String? emailContent) {
emailContentsViewState(Right(TransformHtmlEmailContentLoading()));
if (emailContent?.isEmpty == true) {
consumeState(Stream.value(Left(TransformHtmlEmailContentFailure(EmptyEmailContentException()))));
} else {
consumeState(_transformHtmlEmailContentInteractor.execute(
emailContent!,
TransformConfiguration.forReplyForwardEmail()
));
}
}
String getEmailAddressSender() {
final arguments = composerArguments.value;
if (arguments != null) {
if (arguments.emailActionType == EmailActionType.edit) {
if (arguments.emailActionType == EmailActionType.editDraft) {
return arguments.presentationEmail?.from?.first.emailAddress ?? '';
} else {
return mailboxDashBoardController.userProfile.value?.email ?? '';
@@ -15,9 +15,11 @@ import 'package:get/get.dart';
import 'package:model/email/email_action_type.dart';
import 'package:rich_text_composer/rich_text_composer.dart';
import 'package:rich_text_composer/views/widgets/rich_text_keyboard_toolbar.dart';
import 'package:tmail_ui_user/features/base/widget/cupertino_loading_widget.dart';
import 'package:tmail_ui_user/features/composer/presentation/base_composer_view.dart';
import 'package:tmail_ui_user/features/composer/presentation/styles/composer_style.dart';
import 'package:tmail_ui_user/features/email/domain/state/get_email_content_state.dart';
import 'package:tmail_ui_user/features/email/domain/state/transform_html_email_content_state.dart';
import 'package:tmail_ui_user/main/localizations/app_localizations.dart';
import 'package:tmail_ui_user/main/utils/app_utils.dart';
@@ -276,38 +278,96 @@ class ComposerView extends BaseComposerView {
switch(argsComposer.emailActionType) {
case EmailActionType.compose:
case EmailActionType.composeFromEmailAddress:
return _buildHtmlEditor(context, initialContent: HtmlExtension.editorStartTags);
case EmailActionType.edit:
return controller.emailContentsViewState.value.fold(
(failure) => _buildHtmlEditor(context, initialContent: HtmlExtension.editorStartTags),
case EmailActionType.composeFromFileShared:
return _buildHtmlEditor(
context,
initialContent: HtmlExtension.editorStartTags
);
case EmailActionType.editDraft:
case EmailActionType.editSendingEmail:
case EmailActionType.composeFromContentShared:
case EmailActionType.reopenComposerBrowser:
final emailContentsViewState = controller.emailContentsViewState.value;
if (emailContentsViewState == null) {
return const SizedBox.shrink();
}
return emailContentsViewState.fold(
(failure) => _buildHtmlEditor(
context,
initialContent: HtmlExtension.editorStartTags
),
(success) {
if (success is GetEmailContentLoading) {
return Padding(
padding: const EdgeInsets.all(16.0),
child: loadingWidget,
);
return const CupertinoLoadingWidget(padding: EdgeInsets.all(16.0));
} else if (success is GetEmailContentSuccess) {
var contentHtml = success.emailContent;
var contentHtml = success.htmlEmailContent;
if (contentHtml.isEmpty == true) {
contentHtml = HtmlExtension.editorStartTags;
}
return _buildHtmlEditor(context, initialContent: contentHtml);
return _buildHtmlEditor(
context,
initialContent: contentHtml
);
} else {
return _buildHtmlEditor(context, initialContent: HtmlExtension.editorStartTags);
return _buildHtmlEditor(
context,
initialContent: HtmlExtension.editorStartTags
);
}
});
case EmailActionType.reply:
case EmailActionType.replyAll:
case EmailActionType.forward:
var contentHtml = controller.getEmailContentQuotedAsHtml(
context,
argsComposer);
if (contentHtml.isEmpty == true) {
contentHtml = HtmlExtension.editorStartTags;
final emailContentsViewState = controller.emailContentsViewState.value;
if (emailContentsViewState == null) {
return const SizedBox.shrink();
}
return _buildHtmlEditor(context, initialContent: contentHtml);
return emailContentsViewState.fold(
(failure) {
final emailContentQuoted = controller.getEmailContentQuotedAsHtml(
context: context,
emailContent: '',
emailActionType: argsComposer.emailActionType,
presentationEmail: argsComposer.presentationEmail!
);
return _buildHtmlEditor(
context,
initialContent: emailContentQuoted
);
},
(success) {
if (success is TransformHtmlEmailContentLoading) {
return const CupertinoLoadingWidget(padding: EdgeInsets.all(16.0));
} else if (success is TransformHtmlEmailContentSuccess) {
final emailContentQuoted = controller.getEmailContentQuotedAsHtml(
context: context,
emailContent: success.htmlContent,
emailActionType: argsComposer.emailActionType,
presentationEmail: argsComposer.presentationEmail!
);
return _buildHtmlEditor(
context,
initialContent: emailContentQuoted
);
} else {
final emailContentQuoted = controller.getEmailContentQuotedAsHtml(
context: context,
emailContent: '',
emailActionType: argsComposer.emailActionType,
presentationEmail: argsComposer.presentationEmail!
);
return _buildHtmlEditor(
context,
initialContent: emailContentQuoted
);
}
}
);
default:
return _buildHtmlEditor(context, initialContent: HtmlExtension.editorStartTags);
return _buildHtmlEditor(
context,
initialContent: HtmlExtension.editorStartTags
);
}
});
}
@@ -10,12 +10,14 @@ import 'package:flutter_svg/flutter_svg.dart';
import 'package:get/get.dart';
import 'package:model/email/email_action_type.dart';
import 'package:pointer_interceptor/pointer_interceptor.dart';
import 'package:tmail_ui_user/features/base/widget/cupertino_loading_widget.dart';
import 'package:tmail_ui_user/features/composer/presentation/base_composer_view.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/widgets/email_editor_widget.dart';
import 'package:tmail_ui_user/features/composer/presentation/widgets/toolbar_rich_text_builder.dart';
import 'package:tmail_ui_user/features/email/domain/state/get_email_content_state.dart';
import 'package:tmail_ui_user/features/email/domain/state/transform_html_email_content_state.dart';
import 'package:tmail_ui_user/main/localizations/app_localizations.dart';
import 'package:tmail_ui_user/main/utils/app_utils.dart';
@@ -385,46 +387,96 @@ class ComposerView extends BaseComposerView {
switch(argsComposer.emailActionType) {
case EmailActionType.compose:
case EmailActionType.composeFromEmailAddress:
case EmailActionType.composeFromFileShared:
return _buildHtmlEditor(
context,
currentTextEditor ?? HtmlExtension.editorStartTags);
case EmailActionType.edit:
return controller.emailContentsViewState.value.fold(
context,
currentTextEditor ?? HtmlExtension.editorStartTags
);
case EmailActionType.editDraft:
case EmailActionType.editSendingEmail:
case EmailActionType.composeFromContentShared:
case EmailActionType.reopenComposerBrowser:
final emailContentsViewState = controller.emailContentsViewState.value;
if (emailContentsViewState == null) {
return const SizedBox.shrink();
}
return emailContentsViewState.fold(
(failure) => _buildHtmlEditor(
context,
currentTextEditor ?? HtmlExtension.editorStartTags),
context,
currentTextEditor ?? HtmlExtension.editorStartTags
),
(success) {
if (success is GetEmailContentLoading) {
return Padding(
padding: const EdgeInsets.all(16.0),
child: loadingWidget,
);
return const CupertinoLoadingWidget(padding: EdgeInsets.all(16.0));
} else if (success is GetEmailContentSuccess) {
var contentHtml = success.emailContent;
var contentHtml = success.htmlEmailContent;
if (contentHtml.isEmpty == true) {
contentHtml = HtmlExtension.editorStartTags;
}
return _buildHtmlEditor(context, currentTextEditor ?? contentHtml);
return _buildHtmlEditor(
context,
currentTextEditor ?? contentHtml
);
} else {
return _buildHtmlEditor(
context,
currentTextEditor ?? HtmlExtension.editorStartTags);
currentTextEditor ?? HtmlExtension.editorStartTags
);
}
});
case EmailActionType.reply:
case EmailActionType.replyAll:
case EmailActionType.forward:
var contentHtml = controller.getEmailContentQuotedAsHtml(
context,
argsComposer);
if (contentHtml.isEmpty == true) {
contentHtml = HtmlExtension.editorStartTags;
final emailContentsViewState = controller.emailContentsViewState.value;
if (emailContentsViewState == null) {
return const SizedBox.shrink();
}
return _buildHtmlEditor(context, currentTextEditor ?? contentHtml);
return emailContentsViewState.fold(
(failure) {
final emailContentQuoted = controller.getEmailContentQuotedAsHtml(
context: context,
emailContent: '',
emailActionType: argsComposer.emailActionType,
presentationEmail: argsComposer.presentationEmail!
);
return _buildHtmlEditor(
context,
currentTextEditor ?? emailContentQuoted
);
},
(success) {
if (success is TransformHtmlEmailContentLoading) {
return const CupertinoLoadingWidget(padding: EdgeInsets.all(16.0));
} else if (success is TransformHtmlEmailContentSuccess) {
final emailContentQuoted = controller.getEmailContentQuotedAsHtml(
context: context,
emailContent: success.htmlContent,
emailActionType: argsComposer.emailActionType,
presentationEmail: argsComposer.presentationEmail!
);
return _buildHtmlEditor(
context,
currentTextEditor ?? emailContentQuoted
);
} else {
final emailContentQuoted = controller.getEmailContentQuotedAsHtml(
context: context,
emailContent: '',
emailActionType: argsComposer.emailActionType,
presentationEmail: argsComposer.presentationEmail!
);
return _buildHtmlEditor(
context,
currentTextEditor ?? emailContentQuoted
);
}
}
);
default:
return _buildHtmlEditor(
context,
currentTextEditor ?? HtmlExtension.editorStartTags);
context,
currentTextEditor ?? HtmlExtension.editorStartTags
);
}
});
}
@@ -1,25 +1,35 @@
import 'package:core/presentation/extensions/html_extension.dart';
import 'package:flutter/cupertino.dart';
import 'package:model/email/email_action_type.dart';
import 'package:model/email/presentation_email.dart';
import 'package:model/extensions/list_email_address_extension.dart';
import 'package:model/extensions/utc_date_extension.dart';
import 'package:tmail_ui_user/main/localizations/app_localizations.dart';
extension EmailActionTypeExtension on EmailActionType {
String getSubjectComposer(BuildContext context, String subject) {
String getSubjectComposer(BuildContext? context, String subject) {
switch(this) {
case EmailActionType.reply:
case EmailActionType.replyAll:
if (subject.toLowerCase().startsWith('re:')) {
return subject;
} else {
return '${AppLocalizations.of(context).prefix_reply_email} $subject';
return context != null
? '${AppLocalizations.of(context).prefix_reply_email} $subject'
: subject;
}
case EmailActionType.forward:
if (subject.toLowerCase().startsWith('fwd:')) {
return subject;
} else {
return '${AppLocalizations.of(context).prefix_forward_email} $subject';
return context != null
? '${AppLocalizations.of(context).prefix_forward_email} $subject'
: subject;
}
case EmailActionType.edit:
case EmailActionType.editDraft:
case EmailActionType.editSendingEmail:
case EmailActionType.reopenComposerBrowser:
return subject;
default:
return '';
@@ -40,4 +50,71 @@ extension EmailActionTypeExtension on EmailActionType {
return '';
}
}
String? getHeaderEmailQuoted({
required BuildContext context,
required PresentationEmail presentationEmail
}) {
final locale = Localizations.localeOf(context).toLanguageTag();
switch(this) {
case EmailActionType.reply:
case EmailActionType.replyAll:
final receivedAt = presentationEmail.receivedAt;
final emailAddress = presentationEmail.from.listEmailAddressToString(isFullEmailAddress: true);
return AppLocalizations.of(context).header_email_quoted(
receivedAt.formatDateToLocal(pattern: 'MMM d, y h:mm a', locale: locale),
emailAddress
);
case EmailActionType.forward:
var headerQuoted = '------- ${AppLocalizations.of(context).forwarded_message} -------'.addNewLineTag();
final subject = presentationEmail.subject ?? '';
final receivedAt = presentationEmail.receivedAt;
final fromEmailAddress = presentationEmail.from.listEmailAddressToString(isFullEmailAddress: true);
final toEmailAddress = presentationEmail.to.listEmailAddressToString(isFullEmailAddress: true);
final ccEmailAddress = presentationEmail.cc.listEmailAddressToString(isFullEmailAddress: true);
final bccEmailAddress = presentationEmail.bcc.listEmailAddressToString(isFullEmailAddress: true);
if (subject.isNotEmpty) {
headerQuoted = headerQuoted
.append('${AppLocalizations.of(context).subject_email}: ')
.append(subject)
.addNewLineTag();
}
if (receivedAt != null) {
headerQuoted = headerQuoted
.append('${AppLocalizations.of(context).date}: ')
.append(receivedAt.formatDateToLocal(pattern: 'MMM d, y h:mm a', locale: locale))
.addNewLineTag();
}
if (fromEmailAddress.isNotEmpty) {
headerQuoted = headerQuoted
.append('${AppLocalizations.of(context).from_email_address_prefix}: ')
.append(fromEmailAddress)
.addNewLineTag();
}
if (toEmailAddress.isNotEmpty) {
headerQuoted = headerQuoted
.append('${AppLocalizations.of(context).to_email_address_prefix}: ')
.append(toEmailAddress)
.addNewLineTag();
}
if (ccEmailAddress.isNotEmpty) {
headerQuoted = headerQuoted
.append('${AppLocalizations.of(context).cc_email_address_prefix}: ')
.append(ccEmailAddress)
.addNewLineTag();
}
if (bccEmailAddress.isNotEmpty) {
headerQuoted = headerQuoted
.append('${AppLocalizations.of(context).bcc_email_address_prefix}: ')
.append(bccEmailAddress)
.addNewLineTag();
}
return headerQuoted;
default:
return null;
}
}
}
@@ -99,7 +99,7 @@ class ComposerStyle {
if (responsiveUtils.isPortraitMobile(context) || responsiveUtils.isLandscapeMobile(context)) {
return const EdgeInsetsDirectional.symmetric(horizontal: 16);
} else {
return const EdgeInsetsDirectional.only(start: 88, end: 48);
return const EdgeInsetsDirectional.only(start: 88, end: 48, top: 8);
}
}
@@ -1,12 +1,16 @@
import 'package:core/presentation/utils/html_transformer/transform_configuration.dart';
import 'package:model/model.dart';
abstract class HtmlDataSource {
Future<EmailContent> transformEmailContent(
EmailContent emailContent,
Map<String, String> mapUrlDownloadCID,
{bool draftsEmail = false}
Map<String, String> mapCidImageDownloadUrl,
TransformConfiguration transformConfiguration
);
Future<EmailContent> addTooltipWhenHoverOnLink(EmailContent emailContent);
Future<String> transformHtmlEmailContent(
String htmlContent,
TransformConfiguration configuration
);
}
@@ -1,3 +1,4 @@
import 'package:core/presentation/utils/html_transformer/transform_configuration.dart';
import 'package:model/email/email_content.dart';
import 'package:tmail_ui_user/features/email/data/datasource/html_datasource.dart';
import 'package:tmail_ui_user/features/email/data/local/html_analyzer.dart';
@@ -13,22 +14,25 @@ class HtmlDataSourceImpl extends HtmlDataSource {
@override
Future<EmailContent> transformEmailContent(
EmailContent emailContent,
Map<String, String>? mapUrlDownloadCID,
{bool draftsEmail = false}
Map<String, String> mapCidImageDownloadUrl,
TransformConfiguration transformConfiguration
) {
return Future.sync(() async {
return await _htmlAnalyzer.transformEmailContent(
emailContent,
mapUrlDownloadCID,
draftsEmail: draftsEmail
mapCidImageDownloadUrl,
transformConfiguration
);
}).catchError(_exceptionThrower.throwException);
}
@override
Future<EmailContent> addTooltipWhenHoverOnLink(EmailContent emailContent) {
Future<String> transformHtmlEmailContent(String htmlContent, TransformConfiguration configuration) {
return Future.sync(() async {
return await _htmlAnalyzer.addTooltipWhenHoverOnLink(emailContent);
return await _htmlAnalyzer.transformHtmlEmailContent(
htmlContent,
configuration
);
}).catchError(_exceptionThrower.throwException);
}
}
@@ -1,11 +1,5 @@
import 'dart:convert';
import 'package:collection/collection.dart';
import 'package:core/data/network/dio_client.dart';
import 'package:core/presentation/utils/html_transformer/dom/add_tooltip_link_transformers.dart';
import 'package:core/presentation/utils/html_transformer/html_transform.dart';
import 'package:core/presentation/utils/html_transformer/text/sanitize_autolink_html_transformers.dart';
import 'package:core/presentation/utils/html_transformer/transform_configuration.dart';
import 'package:html/parser.dart';
import 'package:model/email/email_content.dart';
@@ -14,32 +8,27 @@ import 'package:model/email/email_content_type.dart';
class HtmlAnalyzer {
final HtmlTransform _htmlTransform;
final HtmlEscape _htmlEscape;
HtmlAnalyzer(this._htmlTransform, this._htmlEscape);
HtmlAnalyzer(this._htmlTransform);
Future<EmailContent> transformEmailContent(
EmailContent emailContent,
Map<String, String>? mapUrlDownloadCID,
{bool draftsEmail = false}
Map<String, String> mapCidImageDownloadUrl,
TransformConfiguration transformConfiguration
) async {
switch(emailContent.type) {
case EmailContentType.textHtml:
final htmlContent = await _htmlTransform.transformToHtml(
contentHtml: emailContent.content,
mapUrlDownloadCID: mapUrlDownloadCID,
transformConfiguration: draftsEmail
? TransformConfiguration.create(customDomTransformers: TransformConfiguration.domTransformersForDraftEmail)
: null
htmlContent: emailContent.content,
mapCidImageDownloadUrl: mapCidImageDownloadUrl,
transformConfiguration: transformConfiguration
);
return EmailContent(emailContent.type, htmlContent);
case EmailContentType.textPlain:
final message = _htmlTransform.transformToTextPlain(
content: emailContent.content,
transformConfiguration: TransformConfiguration.create(
customTextTransformers: [SanitizeAutolinkHtmlTransformers(_htmlEscape)]
)
transformConfiguration: transformConfiguration
);
return EmailContent(emailContent.type, message);
default:
@@ -47,21 +36,6 @@ class HtmlAnalyzer {
}
}
Future<EmailContent> addTooltipWhenHoverOnLink(EmailContent emailContent) async {
switch(emailContent.type) {
case EmailContentType.textHtml:
final htmlContent = await _htmlTransform.transformToHtml(
contentHtml: emailContent.content,
transformConfiguration: TransformConfiguration.create(
customDomTransformers: [const AddTooltipLinkTransformer()]
)
);
return EmailContent(emailContent.type, htmlContent);
default:
return emailContent;
}
}
Future<List<String>> getListLinkCalendarEvent(String emailContents) async {
final document = parse(emailContents);
final linkElements = document.querySelectorAll('a.part-button');
@@ -71,4 +45,15 @@ class HtmlAnalyzer {
.toList();
return listLink;
}
Future<String> transformHtmlEmailContent(
String htmlContent,
TransformConfiguration configuration
) async {
final htmlContentTransformed = await _htmlTransform.transformToHtml(
htmlContent: htmlContent,
transformConfiguration: configuration
);
return htmlContentTransformed;
}
}
@@ -6,6 +6,7 @@ import 'package:core/data/model/source_type/data_source_type.dart';
import 'package:core/data/network/download/downloaded_response.dart';
import 'package:core/presentation/state/failure.dart';
import 'package:core/presentation/state/success.dart';
import 'package:core/presentation/utils/html_transformer/transform_configuration.dart';
import 'package:dartz/dartz.dart';
import 'package:dio/dio.dart';
import 'package:jmap_dart_client/jmap/account_id.dart';
@@ -109,22 +110,16 @@ class EmailRepositoryImpl extends EmailRepository {
@override
Future<List<EmailContent>> transformEmailContent(
List<EmailContent> emailContents,
List<Attachment> attachmentInlines,
String? baseUrlDownload,
AccountId accountId,
{bool draftsEmail = false}
) async {
final mapUrlDownloadCID = {
for (var attachment in attachmentInlines)
attachment.cid! : attachment.getDownloadUrl(baseUrlDownload!, accountId)
};
List<EmailContent> emailContents,
Map<String, String> mapCidImageDownloadUrl,
TransformConfiguration transformConfiguration
) async {
return await Future.wait(emailContents
.map((emailContent) async {
return await _htmlDataSource.transformEmailContent(
emailContent,
mapUrlDownloadCID,
draftsEmail: draftsEmail
mapCidImageDownloadUrl,
transformConfiguration,
);
})
.toList());
@@ -173,13 +168,6 @@ class EmailRepositoryImpl extends EmailRepository {
return emailDataSource[DataSourceType.network]!.deleteEmailPermanently(session, accountId, emailId);
}
@override
Future<List<EmailContent>> addTooltipWhenHoverOnLink(List<EmailContent> emailContents) {
return Future.wait(emailContents
.map((emailContent) => _htmlDataSource.addTooltipWhenHoverOnLink(emailContent))
.toList());
}
@override
Future<jmap.State?> getEmailState(Session session, AccountId accountId) {
return _stateDataSource.getState(accountId, session.username, StateType.email);
@@ -219,4 +207,9 @@ class EmailRepositoryImpl extends EmailRepository {
Future<DetailedEmail> getStoredNewEmail(Session session, AccountId accountId, EmailId emailId) {
return emailDataSource[DataSourceType.hiveCache]!.getStoredNewEmail(session, accountId, emailId);
}
@override
Future<String> transformHtmlEmailContent(String htmlContent, TransformConfiguration configuration) {
return _htmlDataSource.transformHtmlEmailContent(htmlContent, configuration);
}
}
@@ -3,4 +3,6 @@ class CannotDeleteOldEmailException implements Exception {}
class NotFoundEmailException implements Exception {}
class NotFoundEmailContentException implements Exception {}
class NotFoundEmailContentException implements Exception {}
class EmptyEmailContentException implements Exception {}
@@ -4,6 +4,7 @@ import 'dart:typed_data';
import 'package:core/data/network/download/downloaded_response.dart';
import 'package:core/presentation/state/failure.dart';
import 'package:core/presentation/state/success.dart';
import 'package:core/presentation/utils/html_transformer/transform_configuration.dart';
import 'package:dartz/dartz.dart';
import 'package:dio/dio.dart';
import 'package:jmap_dart_client/jmap/account_id.dart';
@@ -69,14 +70,10 @@ abstract class EmailRepository {
Future<List<EmailContent>> transformEmailContent(
List<EmailContent> emailContents,
List<Attachment> attachmentInlines,
String? baseUrlDownload,
AccountId accountId,
{bool draftsEmail = false}
Map<String, String> mapCidImageDownloadUrl,
TransformConfiguration transformConfiguration
);
Future<List<EmailContent>> addTooltipWhenHoverOnLink(List<EmailContent> emailContents);
Future<Email> saveEmailAsDrafts(Session session, AccountId accountId, Email email);
Future<bool> removeEmailDrafts(Session session, AccountId accountId, EmailId emailId);
@@ -102,4 +99,9 @@ abstract class EmailRepository {
Future<DetailedEmail> getStoredOpenedEmail(Session session, AccountId accountId, EmailId emailId);
Future<DetailedEmail> getStoredNewEmail(Session session, AccountId accountId, EmailId emailId);
Future<String> transformHtmlEmailContent(
String htmlContent,
TransformConfiguration configuration
);
}
@@ -6,41 +6,38 @@ import 'package:model/email/attachment.dart';
class GetEmailContentLoading extends LoadingState {}
class GetEmailContentSuccess extends UIState {
final String emailContent;
final String emailContentDisplayed;
final String htmlEmailContent;
final List<Attachment> attachments;
final Email? emailCurrent;
GetEmailContentSuccess({
required this.emailContent,
required this.emailContentDisplayed,
required this.attachments,
required this.emailCurrent
});
@override
List<Object?> get props => [
emailContent,
emailContentDisplayed,
attachments,
emailCurrent
];
}
class GetEmailContentFromCacheSuccess extends UIState {
final String emailContent;
final List<Attachment> attachments;
final Email? emailCurrent;
GetEmailContentFromCacheSuccess({
required this.emailContent,
required this.htmlEmailContent,
required this.attachments,
this.emailCurrent
});
@override
List<Object?> get props => [
emailContent,
htmlEmailContent,
attachments,
emailCurrent
];
}
class GetEmailContentFromCacheSuccess extends UIState {
final String htmlEmailContent;
final List<Attachment> attachments;
final Email? emailCurrent;
GetEmailContentFromCacheSuccess({
required this.htmlEmailContent,
required this.attachments,
this.emailCurrent
});
@override
List<Object?> get props => [
htmlEmailContent,
attachments,
emailCurrent,
];
@@ -0,0 +1,18 @@
import 'package:core/presentation/state/failure.dart';
import 'package:core/presentation/state/success.dart';
class TransformHtmlEmailContentLoading extends LoadingState {}
class TransformHtmlEmailContentSuccess extends UIState {
final String htmlContent;
TransformHtmlEmailContentSuccess(this.htmlContent);
@override
List<Object?> get props => [htmlContent];
}
class TransformHtmlEmailContentFailure extends FeatureFailure {
TransformHtmlEmailContentFailure(dynamic exception) : super(exception: exception);
}
@@ -1,6 +1,7 @@
import 'package:core/presentation/state/failure.dart';
import 'package:core/presentation/state/success.dart';
import 'package:core/presentation/utils/html_transformer/transform_configuration.dart';
import 'package:core/utils/app_logger.dart';
import 'package:core/utils/platform_info.dart';
import 'package:dartz/dartz.dart';
@@ -20,19 +21,16 @@ class GetEmailContentInteractor {
Session session,
AccountId accountId,
EmailId emailId,
String? baseDownloadUrl,
{
bool composeEmail = false,
bool draftsEmail = false
}
String baseDownloadUrl,
TransformConfiguration transformConfiguration,
) async* {
try {
yield Right<Failure, Success>(GetEmailContentLoading());
if (PlatformInfo.isMobile) {
yield* _getStoredOpenedEmail(session, accountId, emailId, baseDownloadUrl, composeEmail: composeEmail, draftsEmail: draftsEmail);
yield* _getStoredOpenedEmail(session, accountId, emailId, baseDownloadUrl, transformConfiguration);
} else {
yield* _getContentEmailFromServer(session, accountId, emailId, baseDownloadUrl, composeEmail: composeEmail, draftsEmail: draftsEmail);
yield* _getContentEmailFromServer(session, accountId, emailId, baseDownloadUrl, transformConfiguration);
}
} catch (e) {
log('GetEmailContentInteractor::execute(): exception = $e');
@@ -44,38 +42,31 @@ class GetEmailContentInteractor {
Session session,
AccountId accountId,
EmailId emailId,
String? baseDownloadUrl,
{
bool composeEmail = false,
bool draftsEmail = false
}
String baseDownloadUrl,
TransformConfiguration transformConfiguration,
) async* {
try {
final email = await emailRepository.getEmailContent(session, accountId, emailId);
if (email.emailContentList.isNotEmpty) {
final mapCidImageDownloadUrl = email.attachmentsWithCid.toMapCidImageDownloadUrl(
accountId: accountId,
downloadUrl: baseDownloadUrl
);
final newEmailContents = await emailRepository.transformEmailContent(
email.emailContentList,
email.allAttachments.listAttachmentsDisplayedInContent,
baseDownloadUrl,
accountId,
draftsEmail: draftsEmail
mapCidImageDownloadUrl,
transformConfiguration
);
final newEmailContentsDisplayed = PlatformInfo.isWeb && !composeEmail
? await emailRepository.addTooltipWhenHoverOnLink(newEmailContents)
: newEmailContents;
yield Right<Failure, Success>(GetEmailContentSuccess(
emailContent: newEmailContents.asHtmlString,
emailContentDisplayed: newEmailContentsDisplayed.asHtmlString,
htmlEmailContent: newEmailContents.asHtmlString,
attachments: email.allAttachments,
emailCurrent: email
));
} else {
yield Right<Failure, Success>(GetEmailContentSuccess(
emailContent: '',
emailContentDisplayed: '',
htmlEmailContent: '',
attachments: email.allAttachments,
emailCurrent: email
));
@@ -90,17 +81,14 @@ class GetEmailContentInteractor {
Session session,
AccountId accountId,
EmailId emailId,
String? baseDownloadUrl,
{
bool composeEmail = false,
bool draftsEmail = false
}
String baseDownloadUrl,
TransformConfiguration transformConfiguration,
) async* {
try {
log('GetEmailContentInteractor::_getStoredOpenedEmail(): CALLED');
final detailedEmail = await emailRepository.getStoredOpenedEmail(session, accountId, emailId);
yield Right<Failure, Success>(GetEmailContentFromCacheSuccess(
emailContent: detailedEmail.htmlEmailContent ?? "",
htmlEmailContent: detailedEmail.htmlEmailContent ?? '',
attachments: detailedEmail.attachments ?? [],
emailCurrent: Email(
id: emailId,
@@ -115,8 +103,8 @@ class GetEmailContentInteractor {
accountId,
emailId,
baseDownloadUrl,
composeEmail: composeEmail,
draftsEmail: draftsEmail);
transformConfiguration
);
}
}
@@ -124,17 +112,14 @@ class GetEmailContentInteractor {
Session session,
AccountId accountId,
EmailId emailId,
String? baseDownloadUrl,
{
bool composeEmail = false,
bool draftsEmail = false
}
String baseDownloadUrl,
TransformConfiguration transformConfiguration,
) async* {
try {
log('GetEmailContentInteractor::_getStoredNewEmail():CALLED');
final detailedEmail = await emailRepository.getStoredNewEmail(session, accountId, emailId);
yield Right<Failure, Success>(GetEmailContentFromCacheSuccess(
emailContent: detailedEmail.htmlEmailContent ?? "",
htmlEmailContent: detailedEmail.htmlEmailContent ?? '',
attachments: detailedEmail.attachments ?? [],
emailCurrent: Email(
id: emailId,
@@ -149,8 +134,8 @@ class GetEmailContentInteractor {
accountId,
emailId,
baseDownloadUrl,
composeEmail: composeEmail,
draftsEmail: draftsEmail);
transformConfiguration
);
}
}
}
@@ -1,5 +1,7 @@
import 'package:core/presentation/state/failure.dart';
import 'package:core/presentation/state/success.dart';
import 'package:core/presentation/utils/html_transformer/transform_configuration.dart';
import 'package:core/utils/platform_info.dart';
import 'package:dartz/dartz.dart';
import 'package:jmap_dart_client/jmap/account_id.dart';
import 'package:jmap_dart_client/jmap/core/extensions/utc_date_extension.dart';
@@ -23,7 +25,7 @@ class GetListDetailedEmailByIdInteractor {
Session session,
AccountId accountId,
Set<EmailId> emailIds,
String? baseDownloadUrl,
String baseDownloadUrl,
{Set<Comparator>? sort}
) async* {
try {
@@ -57,17 +59,27 @@ class GetListDetailedEmailByIdInteractor {
Future<Tuple2<Email, DetailedEmail>> _parsingEmailToDetailedEmail(
AccountId accountId,
Email email,
String? baseDownloadUrl
String baseDownloadUrl
) async {
String? htmlEmailContent;
final listEmailContent = email.emailContentList;
if (listEmailContent.isNotEmpty) {
final mapCidImageDownloadUrl = email.attachmentsWithCid.toMapCidImageDownloadUrl(
accountId: accountId,
downloadUrl: baseDownloadUrl
);
TransformConfiguration transformConfiguration = TransformConfiguration.standardConfiguration;
if (email.isDraft) {
transformConfiguration = TransformConfiguration.forDraftsEmail();
} else if (PlatformInfo.isWeb) {
transformConfiguration = TransformConfiguration.forPreviewEmailPlatformWeb();
}
final newEmailContents = await _emailRepository.transformEmailContent(
listEmailContent,
email.allAttachments.listAttachmentsDisplayedInContent,
baseDownloadUrl,
accountId);
email.emailContentList,
mapCidImageDownloadUrl,
transformConfiguration
);
htmlEmailContent = newEmailContents.asHtmlString;
}
@@ -0,0 +1,26 @@
import 'package:core/presentation/state/failure.dart';
import 'package:core/presentation/state/success.dart';
import 'package:core/presentation/utils/html_transformer/transform_configuration.dart';
import 'package:dartz/dartz.dart';
import 'package:tmail_ui_user/features/email/domain/repository/email_repository.dart';
import 'package:tmail_ui_user/features/email/domain/state/transform_html_email_content_state.dart';
class TransformHtmlEmailContentInteractor {
final EmailRepository emailRepository;
TransformHtmlEmailContentInteractor(this.emailRepository);
Stream<Either<Failure, Success>> execute(
String htmlContent,
TransformConfiguration configuration
) async* {
try {
yield Right<Failure, Success>(TransformHtmlEmailContentLoading());
final htmlContentTransformed = await emailRepository.transformHtmlEmailContent(htmlContent, configuration);
yield Right<Failure, Success>(TransformHtmlEmailContentSuccess(htmlContentTransformed));
} catch (e) {
yield Left<Failure, Success>(TransformHtmlEmailContentFailure(e));
}
}
}
@@ -360,22 +360,33 @@ class SingleEmailController extends BaseController with AppLoaderMixin {
}
void _getEmailContentAction(EmailId emailId) async {
final session = mailboxDashBoardController.sessionCurrent;
final accountId = mailboxDashBoardController.accountId.value;
final baseDownloadUrl = mailboxDashBoardController.sessionCurrent?.getDownloadUrl(jmapUrl: _dynamicUrlInterceptors.jmapUrl);
final emailLoaded = emailSupervisorController.getEmailInQueueByEmailId(emailId);
if (emailLoaded != null) {
consumeState(Stream.value(Right<Failure, Success>(
GetEmailContentSuccess(
emailContent: emailLoaded.originalContent,
emailContentDisplayed: emailLoaded.displayedContent,
htmlEmailContent: emailLoaded.htmlContent,
attachments: emailLoaded.attachments,
emailCurrent: emailLoaded.emailCurrent
)
)));
} else if (session != null && accountId != null && baseDownloadUrl != null) {
consumeState(_getEmailContentInteractor.execute(session, accountId, emailId, baseDownloadUrl));
} else {
final session = mailboxDashBoardController.sessionCurrent;
final accountId = mailboxDashBoardController.accountId.value;
if (session != null && accountId != null) {
final baseDownloadUrl = mailboxDashBoardController.sessionCurrent?.getDownloadUrl(jmapUrl: _dynamicUrlInterceptors.jmapUrl) ?? '';
TransformConfiguration transformConfiguration = PlatformInfo.isWeb
? TransformConfiguration.forPreviewEmailPlatformWeb()
: TransformConfiguration.standardConfiguration;
consumeState(_getEmailContentInteractor.execute(
session,
accountId,
emailId,
baseDownloadUrl,
transformConfiguration
));
}
}
}
@@ -387,8 +398,7 @@ class SingleEmailController extends BaseController with AppLoaderMixin {
emailSupervisorController.popEmailQueue(success.emailCurrent?.id);
_currentEmailLoaded = EmailLoaded(
originalContent: success.emailContent,
displayedContent: success.emailContent,
htmlContent: success.htmlEmailContent,
attachments: List.of(success.attachments),
emailCurrent: success.emailCurrent,
);
@@ -401,10 +411,10 @@ class SingleEmailController extends BaseController with AppLoaderMixin {
_parseCalendarEventAction(
accountId: mailboxDashBoardController.accountId.value!,
blobIds: success.attachments.calendarEventBlobIds,
emailContents: success.emailContent
emailContents: success.htmlEmailContent
);
} else {
emailContents.value = success.emailContent;
emailContents.value = success.htmlEmailContent;
}
final isShowMessageReadReceipt = success.emailCurrent?.hasReadReceipt(mailboxDashBoardController.mapMailboxById) == true;
@@ -422,8 +432,7 @@ class SingleEmailController extends BaseController with AppLoaderMixin {
emailSupervisorController.popEmailQueue(success.emailCurrent?.id);
_currentEmailLoaded = EmailLoaded(
originalContent: success.emailContent,
displayedContent: success.emailContentDisplayed,
htmlContent: success.htmlEmailContent,
attachments: List.of(success.attachments),
emailCurrent: success.emailCurrent,
);
@@ -436,10 +445,10 @@ class SingleEmailController extends BaseController with AppLoaderMixin {
_parseCalendarEventAction(
accountId: mailboxDashBoardController.accountId.value!,
blobIds: success.attachments.calendarEventBlobIds,
emailContents: success.emailContent
emailContents: success.htmlEmailContent
);
} else {
emailContents.value = success.emailContentDisplayed;
emailContents.value = success.htmlEmailContent;
}
if (PlatformInfo.isMobile) {
@@ -449,7 +458,7 @@ class SingleEmailController extends BaseController with AppLoaderMixin {
attachments: success.attachments,
headers: currentEmail?.emailHeader?.toSet(),
keywords: currentEmail?.keywords,
htmlEmailContent: success.emailContentDisplayed
htmlEmailContent: success.htmlEmailContent
);
_storeOpenedEmailAction(
@@ -978,13 +987,7 @@ class SingleEmailController extends BaseController with AppLoaderMixin {
void composeEmailFromEmailAddress(EmailAddress emailAddress) {
popBack();
final arguments = ComposerArguments(
emailActionType: EmailActionType.composeFromEmailAddress,
emailAddress: emailAddress,
mailboxRole: mailboxDashBoardController.selectedMailbox.value?.role);
mailboxDashBoardController.goToComposer(arguments);
mailboxDashBoardController.goToComposer(ComposerArguments.fromEmailAddress(emailAddress));
}
void openMailToLink(Uri? uri) {
@@ -993,12 +996,7 @@ class SingleEmailController extends BaseController with AppLoaderMixin {
log('SingleEmailController::openMailToLink(): address: $address');
if (address.isNotEmpty) {
final emailAddress = EmailAddress(null, address);
final arguments = ComposerArguments(
emailActionType: EmailActionType.composeFromEmailAddress,
emailAddress: emailAddress,
mailboxRole: mailboxDashBoardController.selectedMailbox.value?.role);
mailboxDashBoardController.goToComposer(arguments);
mailboxDashBoardController.goToComposer(ComposerArguments.fromEmailAddress(emailAddress));
}
}
@@ -1158,19 +1156,43 @@ class SingleEmailController extends BaseController with AppLoaderMixin {
}
}
void pressEmailAction(EmailActionType emailActionType) {
if (emailActionType == EmailActionType.compose) {
mailboxDashBoardController.goToComposer(ComposerArguments());
} else {
final arguments = ComposerArguments(
emailActionType: emailActionType,
presentationEmail: mailboxDashBoardController.selectedEmail.value!,
emailContents: _currentEmailLoaded?.originalContent,
attachments: emailActionType == EmailActionType.forward ? attachments : null,
mailboxRole: mailboxDashBoardController.selectedMailbox.value?.role
);
mailboxDashBoardController.goToComposer(arguments);
void pressEmailAction(
EmailActionType emailActionType,
PresentationEmail presentationEmail
) {
switch(emailActionType) {
case EmailActionType.compose:
mailboxDashBoardController.goToComposer(ComposerArguments());
break;
case EmailActionType.reply:
mailboxDashBoardController.goToComposer(
ComposerArguments.replyEmail(
presentationEmail: presentationEmail,
content: _currentEmailLoaded?.htmlContent ?? '',
mailboxRole: presentationEmail.mailboxContain?.role
)
);
break;
case EmailActionType.replyAll:
mailboxDashBoardController.goToComposer(
ComposerArguments.replyAllEmail(
presentationEmail: presentationEmail,
content: _currentEmailLoaded?.htmlContent ?? '',
mailboxRole: presentationEmail.mailboxContain?.role
)
);
break;
case EmailActionType.forward:
mailboxDashBoardController.goToComposer(
ComposerArguments.forwardEmail(
presentationEmail: presentationEmail,
content: _currentEmailLoaded?.htmlContent ?? '',
attachments: attachments
)
);
break;
default:
break;
}
}
@@ -1276,7 +1298,7 @@ class SingleEmailController extends BaseController with AppLoaderMixin {
void _handleParseCalendarEventFailure(ParseCalendarEventFailure failure) {
emailLoadedViewState.value = Left<Failure, Success>(failure);
emailContents.value = _currentEmailLoaded?.displayedContent;
emailContents.value = _currentEmailLoaded?.htmlContent;
}
void _enableScrollPageView() {
@@ -1289,11 +1311,6 @@ class SingleEmailController extends BaseController with AppLoaderMixin {
void openNewComposerAction(String mailTo) {
final emailAddress = EmailAddress(mailTo, mailTo);
final arguments = ComposerArguments(
emailActionType: EmailActionType.composeFromEmailAddress,
emailAddress: emailAddress,
mailboxRole: mailboxDashBoardController.selectedMailbox.value?.role
);
mailboxDashBoardController.goToComposer(arguments);
mailboxDashBoardController.goToComposer(ComposerArguments.fromEmailAddress(emailAddress));
}
}
@@ -2,6 +2,7 @@ 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:receive_sharing_intent/receive_sharing_intent.dart';
import 'package:tmail_ui_user/features/mailbox_dashboard/data/model/composer_cache.dart';
import 'package:tmail_ui_user/features/sending_queue/domain/model/sending_email.dart';
import 'package:tmail_ui_user/main/routes/router_arguments.dart';
@@ -26,6 +27,84 @@ class ComposerArguments extends RouterArguments {
this.sendingEmail
});
factory ComposerArguments.fromSendingEmail(SendingEmail sendingEmail) =>
ComposerArguments(
emailActionType: EmailActionType.editSendingEmail,
sendingEmail: sendingEmail
);
factory ComposerArguments.fromContentShared(String content) =>
ComposerArguments(
emailActionType: EmailActionType.composeFromContentShared,
emailContents: content
);
factory ComposerArguments.fromFileShared(List<SharedMediaFile> filesShared) =>
ComposerArguments(
emailActionType: EmailActionType.composeFromFileShared,
listSharedMediaFile: filesShared
);
factory ComposerArguments.fromEmailAddress(EmailAddress emailAddress) =>
ComposerArguments(
emailActionType: EmailActionType.composeFromEmailAddress,
emailAddress: emailAddress
);
factory ComposerArguments.editDraftEmail(PresentationEmail presentationEmail) =>
ComposerArguments(
emailActionType: EmailActionType.editDraft,
presentationEmail: presentationEmail
);
factory ComposerArguments.fromSessionStorageBrowser(ComposerCache composerCache) =>
ComposerArguments(
emailActionType: EmailActionType.reopenComposerBrowser,
presentationEmail: PresentationEmail(
id: composerCache.id,
subject: composerCache.subject,
from: composerCache.from,
to: composerCache.to,
cc: composerCache.cc,
bcc: composerCache.bcc,
),
emailContents: composerCache.emailContentList.asHtmlString,
);
factory ComposerArguments.replyEmail({
required PresentationEmail presentationEmail,
required String content,
Role? mailboxRole,
}) => ComposerArguments(
emailActionType: EmailActionType.reply,
presentationEmail: presentationEmail,
emailContents: content,
mailboxRole: mailboxRole
);
factory ComposerArguments.replyAllEmail({
required PresentationEmail presentationEmail,
required String content,
Role? mailboxRole,
}) => ComposerArguments(
emailActionType: EmailActionType.replyAll,
presentationEmail: presentationEmail,
emailContents: content,
mailboxRole: mailboxRole
);
factory ComposerArguments.forwardEmail({
required PresentationEmail presentationEmail,
required String content,
required List<Attachment> attachments,
}) => ComposerArguments(
emailActionType: EmailActionType.forward,
presentationEmail: presentationEmail,
emailContents: content,
attachments: attachments,
mailboxRole: presentationEmail.mailboxContain?.role
);
@override
List<Object?> get props => [
emailActionType,
@@ -3,22 +3,19 @@ import 'package:jmap_dart_client/jmap/mail/email/email.dart';
import 'package:model/email/attachment.dart';
class EmailLoaded with EquatableMixin {
final String originalContent;
final String displayedContent;
final String htmlContent;
final List<Attachment> attachments;
final Email? emailCurrent;
EmailLoaded({
required this.originalContent,
required this.displayedContent,
required this.htmlContent,
required this.attachments,
this.emailCurrent,
});
@override
List<Object?> get props => [
originalContent,
displayedContent,
htmlContent,
attachments,
emailCurrent
];
@@ -9,7 +9,10 @@ import 'package:model/email/presentation_email.dart';
import 'package:model/extensions/presentation_email_extension.dart';
import 'package:tmail_ui_user/main/localizations/app_localizations.dart';
typedef OnPressEmailActionClick = void Function(EmailActionType emailActionType);
typedef OnPressEmailActionClick = void Function(
EmailActionType emailActionType,
PresentationEmail presentationEmail
);
class BottomBarMailWidgetBuilder extends StatelessWidget {
@@ -60,7 +63,7 @@ class BottomBarMailWidgetBuilder extends StatelessWidget {
color: AppColor.colorTextButton
),
verticalDirection: responsiveUtils.isPortraitMobile(context),
onTapActionCallback: () => onPressEmailActionClick?.call(EmailActionType.replyAll),
onTapActionCallback: () => onPressEmailActionClick?.call(EmailActionType.replyAll, _presentationEmail),
),
),
Expanded(
@@ -79,7 +82,7 @@ class BottomBarMailWidgetBuilder extends StatelessWidget {
color: AppColor.colorTextButton
),
verticalDirection: responsiveUtils.isPortraitMobile(context),
onTapActionCallback: () => onPressEmailActionClick?.call(EmailActionType.reply),
onTapActionCallback: () => onPressEmailActionClick?.call(EmailActionType.reply, _presentationEmail),
),
),
Expanded(
@@ -98,7 +101,7 @@ class BottomBarMailWidgetBuilder extends StatelessWidget {
color: AppColor.colorTextButton
),
verticalDirection: responsiveUtils.isPortraitMobile(context),
onTapActionCallback: () => onPressEmailActionClick?.call(EmailActionType.forward),
onTapActionCallback: () => onPressEmailActionClick?.call(EmailActionType.forward, _presentationEmail),
),
),
Expanded(
@@ -117,7 +120,7 @@ class BottomBarMailWidgetBuilder extends StatelessWidget {
color: AppColor.colorTextButton
),
verticalDirection: responsiveUtils.isPortraitMobile(context),
onTapActionCallback: () => onPressEmailActionClick?.call(EmailActionType.compose),
onTapActionCallback: () => onPressEmailActionClick?.call(EmailActionType.compose, _presentationEmail),
),
),
]
@@ -1,4 +1,5 @@
import 'dart:convert';
import 'package:collection/collection.dart';
import 'package:core/core.dart';
import 'package:jmap_dart_client/jmap/mail/email/email.dart';
import 'package:model/model.dart';
@@ -11,13 +12,9 @@ class SessionStorageComposerDatasourceImpl
@override
ComposerCache getComposerCacheOnWeb() {
try {
final result = html.window.sessionStorage.entries
.where((e) => e.key == EmailActionType.edit.name)
.toList();
if (result.isNotEmpty) {
final jsonHandle =
json.decode(result.first.value) as Map<String, dynamic>;
final emailCache = ComposerCache.fromJson(jsonHandle);
final result = html.window.sessionStorage.entries.firstWhereOrNull((e) => e.key == EmailActionType.reopenComposerBrowser.name);
if (result != null) {
final emailCache = ComposerCache.fromJson(jsonDecode(result.value));
return emailCache;
} else {
throw NotFoundInWebSessionException();
@@ -30,8 +27,7 @@ class SessionStorageComposerDatasourceImpl
@override
void removeComposerCacheOnWeb() {
try {
html.window.sessionStorage
.removeWhere((key, value) => key == EmailActionType.edit.name);
html.window.sessionStorage.removeWhere((key, value) => key == EmailActionType.reopenComposerBrowser.name);
} catch (e) {
throw NotFoundInWebSessionException(errorMessage: e.toString());
}
@@ -41,7 +37,7 @@ class SessionStorageComposerDatasourceImpl
void saveComposerCacheOnWeb(Email email) {
try {
Map<String, String> entries = {
EmailActionType.edit.name: json.encode(email.toJson())
EmailActionType.reopenComposerBrowser.name: email.asString()
};
html.window.sessionStorage.addAll(entries);
} catch (e) {
@@ -204,7 +204,8 @@ class MailboxDashBoardController extends ReloadableController {
ComposerArguments? composerArguments;
NavigationRouter? navigationRouter;
late StreamSubscription _emailReceiveManagerStreamSubscription;
late StreamSubscription _emailAddressStreamSubscription;
late StreamSubscription _emailContentStreamSubscription;
late StreamSubscription _fileReceiveManagerStreamSubscription;
final StreamController<Either<Failure, Success>> _progressStateController =
@@ -267,19 +268,7 @@ class MailboxDashBoardController extends ReloadableController {
(failure) {},
(success) {
if(success is GetComposerCacheSuccess){
final ComposerArguments composerArguments = ComposerArguments(
emailActionType: EmailActionType.edit,
presentationEmail: PresentationEmail(
id: success.composerCache.id,
subject: success.composerCache.subject,
from: success.composerCache.from,
to: success.composerCache.to,
cc: success.composerCache.cc,
bcc: success.composerCache.bcc,
),
emailContents: success.composerCache.emailContentList.asHtmlString,
);
openComposerOverlay(composerArguments);
openComposerOverlay(ComposerArguments.fromSessionStorageBrowser(success.composerCache));
}
},
);
@@ -373,49 +362,33 @@ class MailboxDashBoardController extends ReloadableController {
}
void _registerPendingEmailAddress() {
_emailReceiveManagerStreamSubscription =
_emailReceiveManager.pendingEmailAddressInfo.stream.listen((emailAddress) {
log('MailboxDashBoardController::_registerPendingEmailAddress(): ${emailAddress?.email}');
if (emailAddress != null && emailAddress.email?.isNotEmpty == true) {
_emailReceiveManager.clearPendingEmailAddress();
final arguments = ComposerArguments(
emailActionType: EmailActionType.composeFromEmailAddress,
emailAddress: emailAddress,
mailboxRole: selectedMailbox.value?.role);
goToComposer(arguments);
}
});
_emailAddressStreamSubscription =
_emailReceiveManager.pendingEmailAddressInfo.stream.listen((emailAddress) {
if (emailAddress?.email?.isNotEmpty == true) {
_emailReceiveManager.clearPendingEmailAddress();
goToComposer(ComposerArguments.fromEmailAddress(emailAddress!));
}
});
}
void _registerPendingEmailContents() {
_emailReceiveManagerStreamSubscription =
_emailContentStreamSubscription =
_emailReceiveManager.pendingEmailContentInfo.stream.listen((emailContent) {
log('MailboxDashBoardController::_registerPendingEmailContents(): ${emailContent?.content}');
if (emailContent != null && emailContent.content.isNotEmpty == true) {
if (emailContent?.content.isNotEmpty == true) {
_emailReceiveManager.clearPendingEmailContent();
final arguments = ComposerArguments(
emailActionType: EmailActionType.edit,
emailContents: [emailContent].asHtmlString,
mailboxRole: selectedMailbox.value?.role);
goToComposer(arguments);
}
});
goToComposer(ComposerArguments.fromContentShared([emailContent!].asHtmlString));
}
});
}
void _registerPendingFileInfo() {
_fileReceiveManagerStreamSubscription =
_emailReceiveManager.pendingFileInfo.stream.listen((listFile) {
log('MailboxDashBoardController::_registerPendingFileInfo(): ${listFile.length}');
if (listFile.isNotEmpty && sessionCurrent != null) {
_emailReceiveManager.clearPendingFileInfo();
final arguments = ComposerArguments(
emailActionType: EmailActionType.edit,
mailboxRole: selectedMailbox.value?.role,
listSharedMediaFile: listFile,
);
goToComposer(arguments);
}
});
_emailReceiveManager.pendingFileInfo.stream.listen((listFile) {
if (listFile.isNotEmpty) {
_emailReceiveManager.clearPendingFileInfo();
goToComposer(ComposerArguments.fromFileShared(listFile));
}
});
}
void _registerStreamListener() {
@@ -2071,11 +2044,14 @@ class MailboxDashBoardController extends ReloadableController {
void selectAllEmailAction() {
dispatchAction(SelectionAllEmailAction());
}
String get baseDownloadUrl => sessionCurrent?.getDownloadUrl(jmapUrl: dynamicUrlInterceptors.jmapUrl) ?? '';
@override
void onClose() {
_emailReceiveManager.closeEmailReceiveManagerStream();
_emailReceiveManagerStreamSubscription.cancel();
_emailAddressStreamSubscription.cancel();
_emailContentStreamSubscription.cancel();
_fileReceiveManagerStreamSubscription.cancel();
_progressStateController.close();
_refreshActionEventController.close();
@@ -62,7 +62,7 @@ class IdentityDataSourceImpl extends IdentityDataSource {
Future<String> transformHtmlSignature(String signature) {
return Future.sync(() async {
final signatureUnescape = await _htmlTransform.transformToHtml(
contentHtml: signature,
htmlContent: signature,
transformConfiguration: TransformConfiguration.create(customDomTransformers: [
const RemoveScriptTransformer(),
const BlockQuotedTransformer(),
@@ -664,7 +664,7 @@ class SearchEmailController extends BaseController
switch(actionType) {
case EmailActionType.preview:
if (mailboxContain?.isDrafts == true) {
editEmail(selectedEmail);
editDraftEmail(selectedEmail);
} else {
previewEmail(selectedEmail);
}
@@ -10,8 +10,6 @@ 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/user_name.dart';
import 'package:model/email/email_action_type.dart';
import 'package:model/extensions/list_email_content_extension.dart';
import 'package:model/mailbox/select_mode.dart';
import 'package:tmail_ui_user/features/base/base_controller.dart';
import 'package:tmail_ui_user/features/base/mixin/message_dialog_action_mixin.dart';
@@ -270,16 +268,7 @@ class SendingQueueController extends BaseController with MessageDialogActionMixi
void _editSendingEmailAction(SendingEmail sendingEmail) {
disableSelectionMode();
final arguments = ComposerArguments(
emailActionType: EmailActionType.edit,
presentationEmail: sendingEmail.presentationEmail,
mailboxRole: dashboardController?.selectedMailbox.value?.role,
emailContents: sendingEmail.presentationEmail.emailContentList.asHtmlString,
sendingEmail: sendingEmail
);
dashboardController?.goToComposer(arguments);
dashboardController?.goToComposer(ComposerArguments.fromSendingEmail(sendingEmail));
}
void _resendSendingEmailAction(List<SendingEmail> listSendingEmails) async {
@@ -37,13 +37,8 @@ mixin EmailActionController {
final responsiveUtils = Get.find<ResponsiveUtils>();
final imagePaths = Get.find<ImagePaths>();
void editEmail(PresentationEmail presentationEmail) {
final arguments = ComposerArguments(
emailActionType: EmailActionType.edit,
presentationEmail: presentationEmail,
mailboxRole: mailboxDashBoardController.selectedMailbox.value?.role);
mailboxDashBoardController.goToComposer(arguments);
void editDraftEmail(PresentationEmail presentationEmail) {
mailboxDashBoardController.goToComposer(ComposerArguments.editDraftEmail(presentationEmail));
}
void previewEmail(PresentationEmail presentationEmail) {
@@ -873,7 +873,7 @@ class ThreadController extends BaseController with EmailActionController {
switch(actionType) {
case EmailActionType.preview:
if (mailboxContain?.isDrafts == true) {
editEmail(selectedEmail);
editDraftEmail(selectedEmail);
} else {
previewEmail(selectedEmail);
}
@@ -110,7 +110,7 @@ class NetworkBindings extends Bindings {
void _bindingTransformer() {
Get.put(const HtmlEscape());
Get.put(HtmlTransform(Get.find<DioClient>()));
Get.put(HtmlAnalyzer(Get.find<HtmlTransform>(), Get.find<HtmlEscape>()));
Get.put(HtmlTransform(Get.find<DioClient>(), Get.find<HtmlEscape>()));
Get.put(HtmlAnalyzer(Get.find<HtmlTransform>()));
}
}
+5 -1
View File
@@ -9,8 +9,12 @@ enum EmailActionType {
markAsStarred,
unMarkAsStarred,
moveToMailbox,
edit,
editDraft,
editSendingEmail,
composeFromContentShared,
composeFromFileShared,
composeFromEmailAddress,
reopenComposerBrowser,
moveToTrash,
deletePermanently,
preview,
@@ -19,6 +19,8 @@ extension EmailExtension on Email {
bool get hasMdnSent => keywords?.containsKey(KeyWordIdentifier.mdnSent) == true;
bool get isDraft => keywords?.containsKey(KeyWordIdentifier.emailDraft) == true;
bool get withAttachments => hasAttachment == true;
bool hasReadReceipt(Map<MailboxId, PresentationMailbox> mapMailbox) {
@@ -127,6 +129,13 @@ extension EmailExtension on Email {
return [];
}
List<Attachment> get attachmentsWithCid {
return attachments
?.where((element) => element.disposition != null && element.cid?.isNotEmpty == true)
.map((item) => item.toAttachment())
.toList() ?? [];
}
PresentationMailbox? findMailboxContain(Map<MailboxId, PresentationMailbox> mapMailbox) {
final newMailboxIds = mailboxIds;
newMailboxIds?.removeWhere((key, value) => !value);
@@ -1,4 +1,6 @@
import 'package:model/model.dart';
import 'package:jmap_dart_client/jmap/account_id.dart';
import 'package:model/email/attachment.dart';
extension ListAttachmentExtension on List<Attachment> {
@@ -20,4 +22,15 @@ extension ListAttachmentExtension on List<Attachment> {
return where((attachment) => attachment.hasCid())
.toList();
}
Map<String, String> toMapCidImageDownloadUrl({
required AccountId accountId,
required String downloadUrl
}) {
final mapUrlDownloadCID = {
for (var attachment in listAttachmentsDisplayedInContent)
attachment.cid! : attachment.getDownloadUrl(downloadUrl, accountId)
};
return mapUrlDownloadCID;
}
}
@@ -29,9 +29,9 @@ extension ListEmailAddressExtension on Set<EmailAddress>? {
int numberEmailAddress() => this != null ? this!.length : 0;
List<EmailAddress> filterEmailAddress(EmailAddress emailAddressNotExist) {
List<EmailAddress> filterEmailAddress(String emailAddressNotExist) {
return this != null
? this!.where((emailAddress) => emailAddress.email != emailAddressNotExist.email).toList()
? this!.where((emailAddress) => emailAddress.email != emailAddressNotExist).toList()
: List.empty();
}
}
@@ -110,10 +110,10 @@ extension PresentationEmailExtension on PresentationEmail {
return allEmailAddress.isNotEmpty ? allEmailAddress.join(', ') : '';
}
Tuple3<List<EmailAddress>, List<EmailAddress>, List<EmailAddress>> generateRecipientsEmailAddressForComposer(
EmailActionType? emailActionType,
Role? mailboxRole
) {
Tuple3<List<EmailAddress>, List<EmailAddress>, List<EmailAddress>> generateRecipientsEmailAddressForComposer({
required EmailActionType emailActionType,
Role? mailboxRole
}) {
switch(emailActionType) {
case EmailActionType.reply:
if (mailboxRole == PresentationMailbox.roleSent) {
@@ -128,10 +128,8 @@ extension PresentationEmailExtension on PresentationEmail {
} else {
return Tuple3(to.asList() + from.asList(), cc.asList(), bcc.asList());
}
case EmailActionType.edit:
return Tuple3(to.asList(), cc.asList(), bcc.asList());
default:
return const Tuple3([], [], []);
return Tuple3(to.asList(), cc.asList(), bcc.asList());
}
}