TF-2901 Implement caching composer on log out

This commit is contained in:
DatDang
2024-06-18 15:24:02 +07:00
committed by Dat H. Pham
parent c4bffd6626
commit b2c92388f2
18 changed files with 507 additions and 185 deletions
@@ -9,6 +9,13 @@ class NotFoundInWebSessionException with EquatableMixin implements Exception {
List<Object> get props => [];
}
class NotMatchInWebSessionException with EquatableMixin implements Exception {
const NotMatchInWebSessionException();
@override
List<Object> get props => [];
}
class SaveToWebSessionFailException with EquatableMixin implements Exception {
final String? errorMessage;
@@ -48,6 +48,7 @@ class ImageTransformer extends DomTransformer {
imageSource: src
);
imageElement.attributes['src'] = imageBase64 ?? src;
imageElement.attributes['id'] ??= src;
} else if (src.startsWith('https://') || src.startsWith('http://')) {
if (!imageElement.attributes.containsKey('loading')) {
imageElement.attributes['loading'] = 'lazy';
@@ -63,6 +63,8 @@ class TransformConfiguration {
factory TransformConfiguration.forPreviewEmail() => TransformConfiguration.standardConfiguration;
factory TransformConfiguration.forRestoreEmail() => TransformConfiguration.fromDomTransformers([const ImageTransformer()]);
factory TransformConfiguration.forPrintEmail() => TransformConfiguration.fromDomTransformers([
if (PlatformInfo.isWeb)
const RemoveTooltipLinkTransformer(),
@@ -0,0 +1,18 @@
import 'package:core/presentation/state/failure.dart';
import 'package:core/presentation/state/success.dart';
class RestoringEmailInlineImages extends LoadingState {}
class RestoreEmailInlineImagesSuccess extends UIState {
final String emailContent;
RestoreEmailInlineImagesSuccess(this.emailContent);
@override
List<Object?> get props => [emailContent];
}
class RestoreEmailInlineImagesFailure extends FeatureFailure {
RestoreEmailInlineImagesFailure({super.exception});
}
@@ -0,0 +1,30 @@
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/composer/domain/state/restore_email_inline_images_state.dart';
import 'package:tmail_ui_user/features/mailbox_dashboard/domain/repository/composer_cache_repository.dart';
class RestoreEmailInlineImagesInteractor {
RestoreEmailInlineImagesInteractor(this._composerCacheRepository);
final ComposerCacheRepository _composerCacheRepository;
Stream<Either<Failure, Success>> execute({
required String htmlContent,
required TransformConfiguration transformConfiguration,
required Map<String, String> mapUrlDownloadCID
}) async* {
try {
yield Right(RestoringEmailInlineImages());
final emailContent = await _composerCacheRepository.restoreEmailInlineImages(
htmlContent,
transformConfiguration,
mapUrlDownloadCID);
yield Right(RestoreEmailInlineImagesSuccess(emailContent));
} catch (exception) {
yield Left(RestoreEmailInlineImagesFailure(exception: exception));
}
}
}
@@ -1,8 +1,11 @@
import 'package:core/presentation/state/failure.dart';
import 'package:core/presentation/state/success.dart';
import 'package:dartz/dartz.dart';
import 'package:jmap_dart_client/jmap/account_id.dart';
import 'package:jmap_dart_client/jmap/core/user_name.dart';
import 'package:tmail_ui_user/features/composer/domain/repository/composer_repository.dart';
import 'package:tmail_ui_user/features/composer/presentation/model/create_email_request.dart';
import 'package:tmail_ui_user/features/composer/presentation/model/screen_display_mode.dart';
import 'package:tmail_ui_user/features/mailbox_dashboard/domain/repository/composer_cache_repository.dart';
import 'package:tmail_ui_user/features/mailbox_dashboard/domain/state/save_composer_cache_state.dart';
@@ -15,10 +18,22 @@ class SaveComposerCacheOnWebInteractor {
this._composerRepository,
);
Future<Either<Failure, Success>> execute(CreateEmailRequest createEmailRequest) async {
Future<Either<Failure, Success>> execute(
CreateEmailRequest createEmailRequest,
AccountId accountId,
UserName userName,
{required ScreenDisplayMode displayMode}
) async {
try {
final emailCreated = await _composerRepository.generateEmail(createEmailRequest);
_composerCacheRepository.saveComposerCacheOnWeb(emailCreated);
final identity = createEmailRequest.identity;
await _composerCacheRepository.saveComposerCacheOnWeb(
emailCreated,
accountId: accountId,
userName: userName,
displayMode: displayMode,
identity: identity,
readReceipentEnabled: createEmailRequest.isRequestReadReceipt);
return Right(SaveComposerCacheSuccess());
} catch (exception) {
return Left(SaveComposerCacheFailure(exception));
@@ -13,6 +13,7 @@ import 'package:tmail_ui_user/features/composer/domain/repository/contact_reposi
import 'package:tmail_ui_user/features/composer/domain/usecases/create_new_and_save_email_to_drafts_interactor.dart';
import 'package:tmail_ui_user/features/composer/domain/usecases/create_new_and_send_email_interactor.dart';
import 'package:tmail_ui_user/features/composer/domain/usecases/download_image_as_base64_interactor.dart';
import 'package:tmail_ui_user/features/composer/domain/usecases/restore_email_inline_images_interactor.dart';
import 'package:tmail_ui_user/features/composer/domain/usecases/save_composer_cache_on_web_interactor.dart';
import 'package:tmail_ui_user/features/composer/domain/usecases/upload_attachment_interactor.dart';
import 'package:tmail_ui_user/features/composer/presentation/composer_controller.dart';
@@ -208,6 +209,8 @@ class ComposerBindings extends BaseBindings {
Get.find<MailboxRepository>(),
Get.find<ComposerRepository>(),
));
Get.lazyPut(() => RestoreEmailInlineImagesInteractor(
Get.find<ComposerCacheRepository>()));
IdentityInteractorsBindings().dependencies();
}
@@ -15,6 +15,7 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:get/get.dart';
import 'package:jmap_dart_client/jmap/account_id.dart';
import 'package:jmap_dart_client/jmap/identities/identity.dart';
import 'package:jmap_dart_client/jmap/mail/email/email.dart';
import 'package:jmap_dart_client/jmap/mail/email/email_address.dart';
@@ -26,6 +27,8 @@ import 'package:receive_sharing_intent/receive_sharing_intent.dart';
import 'package:rich_text_composer/rich_text_composer.dart';
import 'package:super_tag_editor/tag_editor.dart';
import 'package:tmail_ui_user/features/base/base_controller.dart';
import 'package:tmail_ui_user/features/base/before_unload_handler.dart';
import 'package:tmail_ui_user/features/base/before_unload_manager.dart';
import 'package:tmail_ui_user/features/base/state/base_ui_state.dart';
import 'package:tmail_ui_user/features/base/state/button_state.dart';
import 'package:tmail_ui_user/features/composer/domain/exceptions/compose_email_exception.dart';
@@ -34,6 +37,7 @@ import 'package:tmail_ui_user/features/composer/domain/state/download_image_as_b
import 'package:tmail_ui_user/features/composer/domain/state/generate_email_state.dart';
import 'package:tmail_ui_user/features/composer/domain/state/get_autocomplete_state.dart';
import 'package:tmail_ui_user/features/composer/domain/state/get_device_contact_suggestions_state.dart';
import 'package:tmail_ui_user/features/composer/domain/state/restore_email_inline_images_state.dart';
import 'package:tmail_ui_user/features/composer/domain/state/save_email_as_drafts_state.dart';
import 'package:tmail_ui_user/features/composer/domain/state/send_email_state.dart';
import 'package:tmail_ui_user/features/composer/domain/state/update_email_drafts_state.dart';
@@ -43,6 +47,7 @@ import 'package:tmail_ui_user/features/composer/domain/usecases/download_image_a
import 'package:tmail_ui_user/features/composer/domain/usecases/get_all_autocomplete_interactor.dart';
import 'package:tmail_ui_user/features/composer/domain/usecases/get_autocomplete_interactor.dart';
import 'package:tmail_ui_user/features/composer/domain/usecases/get_device_contact_suggestions_interactor.dart';
import 'package:tmail_ui_user/features/composer/domain/usecases/restore_email_inline_images_interactor.dart';
import 'package:tmail_ui_user/features/composer/domain/usecases/save_composer_cache_on_web_interactor.dart';
import 'package:tmail_ui_user/features/composer/presentation/controller/rich_text_mobile_tablet_controller.dart';
import 'package:tmail_ui_user/features/composer/presentation/controller/rich_text_web_controller.dart';
@@ -91,11 +96,12 @@ import 'package:tmail_ui_user/main/localizations/app_localizations.dart';
import 'package:tmail_ui_user/main/routes/route_navigation.dart';
import 'package:universal_html/html.dart' as html;
class ComposerController extends BaseController with DragDropFileMixin {
class ComposerController extends BaseController with DragDropFileMixin implements BeforeUnloadHandler {
final mailboxDashBoardController = Get.find<MailboxDashBoardController>();
final networkConnectionController = Get.find<NetworkConnectionController>();
final _dynamicUrlInterceptors = Get.find<DynamicUrlInterceptors>();
final _beforeUnloadManager = Get.find<BeforeUnloadManager>();
final composerArguments = Rxn<ComposerArguments>();
final isEnableEmailSendButton = false.obs;
@@ -129,6 +135,7 @@ class ComposerController extends BaseController with DragDropFileMixin {
GetAllAutoCompleteInteractor? _getAllAutoCompleteInteractor;
GetAutoCompleteInteractor? _getAutoCompleteInteractor;
GetDeviceContactSuggestionsInteractor? _getDeviceContactSuggestionsInteractor;
RestoreEmailInlineImagesInteractor? _restoreEmailInlineImagesInteractor;
List<EmailAddress> listToEmailAddress = <EmailAddress>[];
List<EmailAddress> listCcEmailAddress = <EmailAddress>[];
@@ -162,6 +169,7 @@ class ComposerController extends BaseController with DragDropFileMixin {
StreamSubscription<html.Event>? _subscriptionOnDragOver;
StreamSubscription<html.Event>? _subscriptionOnDragLeave;
StreamSubscription<html.Event>? _subscriptionOnDrop;
StreamSubscription<String>? _composerCacheListener;
RichTextMobileTabletController? richTextMobileTabletController;
RichTextWebController? richTextWebController;
@@ -215,6 +223,7 @@ class ComposerController extends BaseController with DragDropFileMixin {
scrollControllerEmailAddress.addListener(_scrollControllerEmailAddressListener);
_listenStreamEvent();
_getAlwaysReadReceiptSetting();
_beforeUnloadManager.addListener(onBeforeUnload);
}
@override
@@ -241,6 +250,8 @@ class ComposerController extends BaseController with DragDropFileMixin {
_subscriptionOnDragLeave?.cancel();
_subscriptionOnDrop?.cancel();
subjectEmailInputFocusNode?.removeListener(_subjectEmailInputFocusListener);
_composerCacheListener?.cancel();
_beforeUnloadManager.removeListener(onBeforeUnload);
super.onClose();
}
@@ -281,7 +292,8 @@ class ComposerController extends BaseController with DragDropFileMixin {
super.handleSuccessViewState(success);
if (success is GetEmailContentLoading ||
success is TransformHtmlEmailContentLoading ||
success is TransformHtmlEmailContentSuccess) {
success is TransformHtmlEmailContentSuccess ||
success is RestoringEmailInlineImages) {
emailContentsViewState.value = Right(success);
} else if (success is LocalFilePickerSuccess) {
_handlePickFileSuccess(success);
@@ -303,9 +315,18 @@ class ComposerController extends BaseController with DragDropFileMixin {
maxWithEditor = null;
} else if (success is GetAlwaysReadReceiptSettingSuccess) {
hasRequestReadReceipt.value = success.alwaysReadReceiptEnabled;
} else if (success is RestoreEmailInlineImagesSuccess) {
_updateEditorContent(success);
}
}
void _updateEditorContent(RestoreEmailInlineImagesSuccess success) {
richTextWebController?.editorController.setText(success.emailContent);
consumeState(Stream.value(Right(GetEmailContentSuccess(
htmlEmailContent: success.emailContent,
attachments: []))));
}
@override
void handleFailureViewState(Failure failure) {
super.handleFailureViewState(failure);
@@ -314,7 +335,8 @@ class ComposerController extends BaseController with DragDropFileMixin {
} else if (failure is LocalImagePickerFailure) {
_handlePickImageFailure(failure);
} else if (failure is GetEmailContentFailure ||
failure is TransformHtmlEmailContentFailure) {
failure is TransformHtmlEmailContentFailure ||
failure is RestoreEmailInlineImagesFailure) {
emailContentsViewState.value = Left(failure);
} else if (failure is GetAllIdentitiesFailure) {
if (identitySelected.value == null) {
@@ -348,40 +370,7 @@ class ComposerController extends BaseController with DragDropFileMixin {
_subscriptionOnBeforeUnload = html.window.onBeforeUnload.listen((event) async {
await _removeComposerCacheOnWebInteractor.execute();
if (composerArguments.value == null ||
mailboxDashBoardController.sessionCurrent == null ||
mailboxDashBoardController.accountId.value == null
) {
log('ComposerController::_listenBrowserTabRefresh: SESSION or ACCOUNT_ID or ARGUMENTS is NULL');
return;
}
final emailContent = await _getContentInEditor();
await _saveComposerCacheOnWebInteractor.execute(CreateEmailRequest(
session: mailboxDashBoardController.sessionCurrent!,
accountId: mailboxDashBoardController.accountId.value!,
emailActionType: composerArguments.value!.emailActionType,
subject: subjectEmail.value ?? '',
emailContent: emailContent,
fromSender: composerArguments.value!.presentationEmail?.from ?? {},
toRecipients: listToEmailAddress.toSet(),
ccRecipients: listCcEmailAddress.toSet(),
bccRecipients: listBccEmailAddress.toSet(),
isRequestReadReceipt: hasRequestReadReceipt.value,
identity: identitySelected.value,
attachments: uploadController.attachmentsUploaded,
inlineAttachments: uploadController.mapInlineAttachments,
outboxMailboxId: mailboxDashBoardController.outboxMailbox?.mailboxId,
sentMailboxId: mailboxDashBoardController.mapDefaultMailboxIdByRole[PresentationMailbox.roleSent],
draftsMailboxId: mailboxDashBoardController.mapDefaultMailboxIdByRole[PresentationMailbox.roleDrafts],
draftsEmailId: _getDraftEmailId(),
answerForwardEmailId: composerArguments.value!.presentationEmail?.id,
unsubscribeEmailId: composerArguments.value!.previousEmailId,
messageId: composerArguments.value!.messageId,
references: composerArguments.value!.references,
emailSendingQueue: composerArguments.value!.sendingEmail
));
await _saveComposerCacheOnWebAction();
});
_subscriptionOnDragEnter = html.window.onDragEnter.listen((event) {
@@ -401,6 +390,56 @@ class ComposerController extends BaseController with DragDropFileMixin {
});
}
Future<void> _saveComposerCacheOnWebAction() async {
_autoCreateEmailTag();
final createEmailRequest = await _generateCreateEmailRequest();
if (createEmailRequest == null) return;
await _saveComposerCacheOnWebInteractor.execute(
createEmailRequest,
mailboxDashBoardController.accountId.value!,
mailboxDashBoardController.sessionCurrent!.username,
displayMode: screenDisplayMode.value);
}
Future<CreateEmailRequest?> _generateCreateEmailRequest() async {
if (composerArguments.value == null ||
mailboxDashBoardController.sessionCurrent == null ||
mailboxDashBoardController.accountId.value == null
) {
log('ComposerController::_generateCreateEmailRequest: SESSION or ACCOUNT_ID or ARGUMENTS is NULL');
return null;
}
final emailContent = await _getContentInEditor();
return CreateEmailRequest(
session: mailboxDashBoardController.sessionCurrent!,
accountId: mailboxDashBoardController.accountId.value!,
emailActionType: composerArguments.value!.emailActionType,
subject: subjectEmail.value ?? '',
emailContent: emailContent,
fromSender: composerArguments.value!.presentationEmail?.from ?? {},
toRecipients: listToEmailAddress.toSet(),
ccRecipients: listCcEmailAddress.toSet(),
bccRecipients: listBccEmailAddress.toSet(),
isRequestReadReceipt: hasRequestReadReceipt.value,
identity: identitySelected.value,
attachments: uploadController.attachmentsUploaded,
inlineAttachments: uploadController.mapInlineAttachments,
outboxMailboxId: mailboxDashBoardController.outboxMailbox?.mailboxId,
sentMailboxId: mailboxDashBoardController.mapDefaultMailboxIdByRole[PresentationMailbox.roleSent],
draftsMailboxId: mailboxDashBoardController.mapDefaultMailboxIdByRole[PresentationMailbox.roleDrafts],
draftsEmailId: _getDraftEmailId(),
answerForwardEmailId: composerArguments.value!.presentationEmail?.id,
unsubscribeEmailId: composerArguments.value!.previousEmailId,
messageId: composerArguments.value!.messageId,
references: composerArguments.value!.references,
emailSendingQueue: composerArguments.value!.sendingEmail
);
}
void _scrollControllerEmailAddressListener() {
if (toEmailAddressController.text.isNotEmpty) {
keyToEmailTagEditor.currentState?.closeSuggestionBox();
@@ -481,7 +520,7 @@ class ComposerController extends BaseController with DragDropFileMixin {
if (arguments is ComposerArguments) {
composerArguments.value = arguments;
_initIdentities(arguments.identities);
_initIdentities(arguments);
injectAutoCompleteBindings(
mailboxDashBoardController.sessionCurrent,
@@ -563,6 +602,10 @@ class ComposerController extends BaseController with DragDropFileMixin {
_transformHtmlEmailContent(arguments.emailContents);
break;
case EmailActionType.reopenComposerBrowser:
if (!PlatformInfo.isWeb) return;
screenDisplayMode.value = arguments.displayMode;
_initEmailAddress(
presentationEmail: arguments.presentationEmail!,
actionType: EmailActionType.reopenComposerBrowser
@@ -571,8 +614,22 @@ class ComposerController extends BaseController with DragDropFileMixin {
presentationEmail: arguments.presentationEmail!,
actionType: EmailActionType.reopenComposerBrowser
);
_initAttachments(arguments.attachments ?? []);
_getEmailContentFromSessionStorageBrowser(arguments.emailContents!);
_initAttachments(
arguments.attachments ?? [],
inlineAttachments: arguments.inlineImages);
final accountId = mailboxDashBoardController.accountId.value;
final downloadUrl = mailboxDashBoardController.sessionCurrent
?.getDownloadUrl(jmapUrl: dynamicUrlInterceptors.jmapUrl);
if (accountId == null || downloadUrl == null) return;
_getEmailContentFromSessionStorageBrowser(
htmlContent: arguments.emailContents ?? '',
inlineImages: arguments.inlineImages ?? [],
accountId: accountId,
downloadUrl: downloadUrl
);
hasRequestReadReceipt.value = arguments.readRecepientEnabled ?? false;
break;
case EmailActionType.composeFromUnsubscribeMailtoLink:
if (arguments.subject != null) {
@@ -603,17 +660,26 @@ class ComposerController extends BaseController with DragDropFileMixin {
subjectEmailInputController.text = newSubject;
}
void _initAttachments(List<Attachment> attachments) {
void _initAttachments(List<Attachment> attachments, {List<Attachment>? inlineAttachments}) {
if (attachments.isNotEmpty) {
initialAttachments = attachments;
uploadController.initializeUploadAttachments(attachments);
}
if (inlineAttachments != null) {
uploadController.initializeUploadInlineAttachments(inlineAttachments);
}
}
void _initIdentities(List<Identity>? identities) {
if (identities?.isNotEmpty == true) {
listFromIdentities.value = identities!;
identitySelected.value = identities.first;
void _initIdentities(ComposerArguments composerArguments) {
listFromIdentities.value = composerArguments.identities ?? [];
if (listFromIdentities.isEmpty) {
_getAllIdentities();
} else if (composerArguments.selectedIdentity != null
&& listFromIdentities.contains(composerArguments.selectedIdentity!)
) {
_selectIdentity(composerArguments.selectedIdentity!);
} else if (composerArguments.identities?.isNotEmpty == true) {
_selectIdentity(composerArguments.identities!.first);
}
}
@@ -632,7 +698,14 @@ class ComposerController extends BaseController with DragDropFileMixin {
listFromIdentities.value = listIdentitiesMayDeleted;
if (identitySelected.value == null) {
await _selectIdentity(listIdentitiesMayDeleted.first);
final selectedIdentityFromArguments = composerArguments.value?.selectedIdentity;
if (selectedIdentityFromArguments != null
&& listFromIdentities.contains(selectedIdentityFromArguments)
) {
await _selectIdentity(selectedIdentityFromArguments);
} else {
await _selectIdentity(listIdentitiesMayDeleted.firstOrNull);
}
}
}
_autoFocusFieldWhenLauncher();
@@ -1226,13 +1299,20 @@ class ComposerController extends BaseController with DragDropFileMixin {
));
}
void _getEmailContentFromSessionStorageBrowser(String content) {
consumeState(Stream.value(
Right(GetEmailContentSuccess(
htmlEmailContent: content,
attachments: [],
))
));
void _getEmailContentFromSessionStorageBrowser({
required String htmlContent,
required List<Attachment> inlineImages,
required AccountId accountId,
required String downloadUrl
}) {
_restoreEmailInlineImagesInteractor = getBinding<RestoreEmailInlineImagesInteractor>();
if (_restoreEmailInlineImagesInteractor == null) return;
consumeState(_restoreEmailInlineImagesInteractor!.execute(
htmlContent: htmlContent,
transformConfiguration: TransformConfiguration.forRestoreEmail(),
mapUrlDownloadCID: inlineImages.toMapCidImageDownloadUrl(
accountId: accountId,
downloadUrl: downloadUrl)));
}
void _getEmailContentFromContentShared(String content) {
@@ -1533,7 +1613,12 @@ class ComposerController extends BaseController with DragDropFileMixin {
Future<void> _selectIdentity(Identity? newIdentity) async {
final formerIdentity = identitySelected.value;
identitySelected.value = newIdentity;
if (newIdentity != null) {
if (newIdentity == null) return;
if (composerArguments.value?.emailActionType == EmailActionType.reopenComposerBrowser) {
composerArguments.value = composerArguments.value?.copyWith(
emailActionType: EmailActionType.editDraft);
} else {
await _applyIdentityForAllFieldComposer(formerIdentity, newIdentity);
}
}
@@ -2178,4 +2263,13 @@ class ComposerController extends BaseController with DragDropFileMixin {
ccRecipientState.value = isEnabled ? PrefixRecipientState.disabled : PrefixRecipientState.enabled;
bccRecipientState.value = isEnabled ? PrefixRecipientState.disabled : PrefixRecipientState.enabled;
}
@override
Future<void> onBeforeUnload() async {
if (mailboxDashBoardController.accountId.value != null &&
mailboxDashBoardController.sessionCurrent?.username != null
) {
await _saveComposerCacheOnWebAction();
}
}
}
@@ -2,5 +2,18 @@
enum ScreenDisplayMode {
fullScreen,
minimize,
normal
normal;
factory ScreenDisplayMode.fromJson(String value) {
switch (value) {
case 'fullScreen':
return ScreenDisplayMode.fullScreen;
case 'minimize':
return ScreenDisplayMode.minimize;
default:
return ScreenDisplayMode.normal;
}
}
String toJson() => name;
}
@@ -3,22 +3,10 @@ import 'package:tmail_ui_user/features/email/presentation/model/composer_argumen
extension ComposerArgumentsExtension on ComposerArguments {
ComposerArguments withIdentity({List<Identity>? identities}) {
return ComposerArguments(
emailActionType: emailActionType,
presentationEmail: presentationEmail,
emailContents: emailContents,
attachments: attachments,
mailboxRole: mailboxRole,
listEmailAddress: listEmailAddress,
listSharedMediaFile: listSharedMediaFile,
sendingEmail: sendingEmail,
subject: subject,
body: body,
messageId: messageId,
references: references,
previousEmailId: previousEmailId,
ComposerArguments withIdentity({List<Identity>? identities, Identity? selectedIdentity}) {
return copyWith(
identities: identities,
selectedIdentity: selectedIdentity,
);
}
}
@@ -4,6 +4,8 @@ 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/composer/presentation/model/screen_display_mode.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/features/sending_queue/presentation/model/sending_email_action_type.dart';
@@ -24,6 +26,10 @@ class ComposerArguments extends RouterArguments {
final MessageIdsHeaderValue? references;
final EmailId? previousEmailId;
final List<Identity>? identities;
final Identity? selectedIdentity;
final List<Attachment>? inlineImages;
final bool? readRecepientEnabled;
final ScreenDisplayMode displayMode;
ComposerArguments({
this.emailActionType = EmailActionType.compose,
@@ -40,6 +46,10 @@ class ComposerArguments extends RouterArguments {
this.references,
this.previousEmailId,
this.identities,
this.selectedIdentity,
this.inlineImages,
this.readRecepientEnabled,
this.displayMode = ScreenDisplayMode.normal
});
factory ComposerArguments.fromSendingEmail(SendingEmail sendingEmail) =>
@@ -83,15 +93,15 @@ class ComposerArguments extends RouterArguments {
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,
presentationEmail: composerCache.email?.toPresentationEmail(),
emailContents: composerCache.email?.emailContentList.asHtmlString,
attachments: composerCache.email?.allAttachments
.where((attachment) => attachment.disposition != ContentDisposition.inline)
.toList(),
selectedIdentity: composerCache.identity,
inlineImages: composerCache.email?.attachmentsWithCid,
readRecepientEnabled: composerCache.readReceipentEnabled,
displayMode: composerCache.displayMode,
);
factory ComposerArguments.replyEmail({
@@ -175,4 +185,46 @@ class ComposerArguments extends RouterArguments {
references,
identities,
];
}
ComposerArguments copyWith({
EmailActionType? emailActionType,
PresentationEmail? presentationEmail,
String? emailContents,
List<SharedMediaFile>? listSharedMediaFile,
List<EmailAddress>? listEmailAddress,
List<Attachment>? attachments,
Role? mailboxRole,
SendingEmail? sendingEmail,
String? subject,
String? body,
MessageIdsHeaderValue? messageId,
MessageIdsHeaderValue? references,
EmailId? previousEmailId,
List<Identity>? identities,
Identity? selectedIdentity,
List<Attachment>? inlineImages,
bool? readRecepientEnabled,
ScreenDisplayMode? displayMode,
}) {
return ComposerArguments(
emailActionType: emailActionType ?? this.emailActionType,
presentationEmail: presentationEmail ?? this.presentationEmail,
emailContents: emailContents ?? this.emailContents,
listSharedMediaFile: listSharedMediaFile ?? this.listSharedMediaFile,
listEmailAddress: listEmailAddress ?? this.listEmailAddress,
attachments: attachments ?? this.attachments,
mailboxRole: mailboxRole ?? this.mailboxRole,
sendingEmail: sendingEmail ?? this.sendingEmail,
subject: subject ?? this.subject,
body: body ?? this.body,
messageId: messageId ?? this.messageId,
references: references ?? this.references,
previousEmailId: previousEmailId ?? this.previousEmailId,
identities: identities ?? this.identities,
selectedIdentity: selectedIdentity ?? this.selectedIdentity,
inlineImages: inlineImages ?? this.inlineImages,
readRecepientEnabled: readRecepientEnabled ?? this.readRecepientEnabled,
displayMode: displayMode ?? this.displayMode,
);
}
}
@@ -1,10 +1,31 @@
import 'package:core/presentation/utils/html_transformer/transform_configuration.dart';
import 'package:jmap_dart_client/jmap/account_id.dart';
import 'package:jmap_dart_client/jmap/core/user_name.dart';
import 'package:jmap_dart_client/jmap/identities/identity.dart';
import 'package:jmap_dart_client/jmap/mail/email/email.dart';
import 'package:tmail_ui_user/features/composer/presentation/model/screen_display_mode.dart';
import 'package:tmail_ui_user/features/mailbox_dashboard/data/model/composer_cache.dart';
abstract class SessionStorageComposerDatasource {
void saveComposerCacheOnWeb(Email email);
Future<void> saveComposerCacheOnWeb(
Email email,
{
required AccountId accountId,
required UserName userName,
required ScreenDisplayMode displayMode,
Identity? identity,
bool? readReceipentEnabled,
}
);
ComposerCache getComposerCacheOnWeb();
Future<ComposerCache> getComposerCacheOnWeb(
AccountId accountId,
UserName userName);
void removeComposerCacheOnWeb();
Future<void> removeComposerCacheOnWeb();
Future<String> restoreEmailInlineImages(
String htmlContent,
TransformConfiguration transformConfiguration,
Map<String, String> mapUrlDownloadCID);
}
@@ -1,47 +1,94 @@
import 'dart:convert';
import 'package:collection/collection.dart';
import 'package:core/core.dart';
import 'package:jmap_dart_client/jmap/account_id.dart';
import 'package:jmap_dart_client/jmap/core/user_name.dart';
import 'package:jmap_dart_client/jmap/identities/identity.dart';
import 'package:jmap_dart_client/jmap/mail/email/email.dart';
import 'package:model/model.dart';
import 'package:tmail_ui_user/features/mailbox_dashboard/data/model/composer_cache.dart';
import 'package:tmail_ui_user/features/caching/utils/cache_utils.dart';
import 'package:tmail_ui_user/features/composer/presentation/model/screen_display_mode.dart';
import 'package:tmail_ui_user/features/mailbox_dashboard/data/datasource/session_storage_composer_datasource.dart';
import 'package:tmail_ui_user/features/mailbox_dashboard/data/model/composer_cache.dart';
import 'package:tmail_ui_user/main/exceptions/exception_thrower.dart';
import 'package:universal_html/html.dart' as html;
class SessionStorageComposerDatasourceImpl
extends SessionStorageComposerDatasource {
SessionStorageComposerDatasourceImpl(this._htmlTransform, this._exceptionThrower);
final HtmlTransform _htmlTransform;
final ExceptionThrower _exceptionThrower;
@override
ComposerCache getComposerCacheOnWeb() {
try {
final result = html.window.sessionStorage.entries.firstWhereOrNull((e) => e.key == EmailActionType.reopenComposerBrowser.name);
Future<ComposerCache> getComposerCacheOnWeb(
AccountId accountId,
UserName userName
) async {
return Future.sync(() async {
final keyWithIdentity = TupleKey(
EmailActionType.reopenComposerBrowser.name,
accountId.asString,
userName.value).toString();
final result = html.window.sessionStorage.entries.firstWhereOrNull(
(entry) => entry.key == keyWithIdentity);
if (result != null) {
final emailCache = ComposerCache.fromJson(jsonDecode(result.value));
return emailCache;
return ComposerCache.fromJson(jsonDecode(result.value));
} else {
throw NotFoundInWebSessionException();
}
} catch (e) {
throw NotFoundInWebSessionException(errorMessage: e.toString());
}
}).catchError(_exceptionThrower.throwException);
}
@override
void removeComposerCacheOnWeb() {
try {
html.window.sessionStorage.removeWhere((key, value) => key == EmailActionType.reopenComposerBrowser.name);
} catch (e) {
throw NotFoundInWebSessionException(errorMessage: e.toString());
}
Future<void> removeComposerCacheOnWeb() async {
return Future.sync(() {
html.window.sessionStorage.removeWhere(
(key, value) => key.startsWith(EmailActionType.reopenComposerBrowser.name));
}).catchError(_exceptionThrower.throwException);
}
@override
void saveComposerCacheOnWeb(Email email) {
try {
Future<void> saveComposerCacheOnWeb(
Email email,
{
required AccountId accountId,
required UserName userName,
required ScreenDisplayMode displayMode,
Identity? identity,
bool? readReceipentEnabled
}
) async {
return Future.sync(() {
final composerCacheKey = TupleKey(
EmailActionType.reopenComposerBrowser.name,
accountId.asString,
userName.value).toString();
Map<String, String> entries = {
EmailActionType.reopenComposerBrowser.name: email.asString()
composerCacheKey: jsonEncode(
ComposerCache(
displayMode: displayMode,
email: email,
identity: identity,
readReceipentEnabled: readReceipentEnabled,
).toJson()
)
};
html.window.sessionStorage.addAll(entries);
} catch (e) {
throw SaveToWebSessionFailException(errorMessage: e.toString());
}
}).catchError(_exceptionThrower.throwException);
}
@override
Future<String> restoreEmailInlineImages(
String htmlContent,
TransformConfiguration transformConfiguration,
Map<String, String> mapUrlDownloadCID) {
return Future.sync(() async {
return await _htmlTransform.transformToHtml(
htmlContent: htmlContent,
transformConfiguration: transformConfiguration,
mapCidImageDownloadUrl: mapUrlDownloadCID);
}).catchError(_exceptionThrower.throwException);
}
}
@@ -1,81 +1,44 @@
import 'package:equatable/equatable.dart';
import 'package:jmap_dart_client/http/converter/email/email_body_value_converter.dart';
import 'package:jmap_dart_client/http/converter/email/email_mailbox_ids_converter.dart';
import 'package:jmap_dart_client/jmap/core/id.dart';
import 'package:jmap_dart_client/jmap/identities/identity.dart';
import 'package:jmap_dart_client/jmap/mail/email/email.dart';
import 'package:jmap_dart_client/jmap/mail/email/email_address.dart';
import 'package:jmap_dart_client/jmap/mail/email/email_body_part.dart';
import 'package:jmap_dart_client/jmap/mail/email/email_body_value.dart';
import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart';
import 'package:model/email/email_content.dart';
import 'package:model/extensions/media_type_nullable_extension.dart';
import 'package:tmail_ui_user/features/composer/presentation/model/screen_display_mode.dart';
class ComposerCache with EquatableMixin {
final EmailId? id;
final Map<MailboxId, bool>? mailboxIds;
final String? subject;
final Set<EmailAddress>? from;
final Set<EmailAddress>? to;
final Set<EmailAddress>? cc;
final Set<EmailAddress>? bcc;
final Set<EmailAddress>? replyTo;
final Set<EmailBodyPart>? textBody;
final Set<EmailBodyPart>? htmlBody;
final Map<PartId, EmailBodyValue>? bodyValues;
final Email? email;
final Identity? identity;
final bool? readReceipentEnabled;
final ScreenDisplayMode displayMode;
ComposerCache({
this.id,
this.mailboxIds,
this.subject,
this.from,
this.to,
this.cc,
this.bcc,
this.replyTo,
this.textBody,
this.htmlBody,
this.bodyValues,
required this.displayMode,
this.email,
this.identity,
this.readReceipentEnabled,
});
factory ComposerCache.fromJson(Map<String, dynamic> json) {
return ComposerCache(
id: json['id'] != null ? EmailId(Id(json['id'])) : null,
mailboxIds: (json['mailboxIds'] as Map<String, dynamic>?)?.map((key, value) => EmailMailboxIdsConverter().parseEntry(key, value)),
subject: json['subject'] as String?,
from: (json['from'] as List<dynamic>?)?.map((json) => EmailAddress.fromJson(json)).toSet(),
to: (json['to'] as List<dynamic>?)?.map((json) => EmailAddress.fromJson(json)).toSet(),
cc: (json['cc'] as List<dynamic>?)?.map((json) => EmailAddress.fromJson(json)).toSet(),
bcc: (json['bcc'] as List<dynamic>?)?.map((json) => EmailAddress.fromJson(json)).toSet(),
bodyValues: (json['bodyValues'] as Map<String, dynamic>?)?.map((key, value) => EmailBodyValueConverter().parseEntry(key, value)),
replyTo: (json['replyTo'] as List<dynamic>?)?.map((json) => EmailAddress.fromJson(json)).toSet(),
textBody: (json['textBody'] as List<dynamic>?)?.map((json) => EmailBodyPart.fromJson(json)).toSet(),
htmlBody: (json['htmlBody'] as List<dynamic>?)?.map((json) => EmailBodyPart.fromJson(json)).toSet(),
);
}
@override
List<Object?> get props => [
id,
subject,
from,
to,
cc,
bcc,
replyTo,
email,
identity,
readReceipentEnabled
];
List<EmailContent> get emailContentList {
final newHtmlBody = htmlBody
?.where((emailBody) => emailBody.partId != null && emailBody.type != null)
.toList() ?? <EmailBodyPart>[];
final mapHtmlBody = { for (var emailBody in newHtmlBody) emailBody.partId! : emailBody.type! };
final emailContents = bodyValues?.entries
.map((entries) => EmailContent(mapHtmlBody[entries.key].toEmailContentType(), entries.value.value))
.toList();
return emailContents ?? [];
Map<String, dynamic> toJson() {
return <String, dynamic>{
'email': email?.toJson(),
'identity': identity?.toJson(),
'readReceipentEnabled': readReceipentEnabled,
'displayMode': displayMode.toJson()
};
}
}
factory ComposerCache.fromJson(Map<String, dynamic> map) {
return ComposerCache(
displayMode: ScreenDisplayMode.fromJson(map['displayMode'] ?? ''),
email: map['email'] != null ? Email.fromJson(map['email']) : null,
identity: map['identity'] != null ? Identity.fromJson(map['identity']) : null,
readReceipentEnabled: map['readReceipentEnabled'] as bool?
);
}
}
@@ -1,4 +1,9 @@
import 'package:core/presentation/utils/html_transformer/transform_configuration.dart';
import 'package:jmap_dart_client/jmap/account_id.dart';
import 'package:jmap_dart_client/jmap/core/user_name.dart';
import 'package:jmap_dart_client/jmap/identities/identity.dart';
import 'package:jmap_dart_client/jmap/mail/email/email.dart';
import 'package:tmail_ui_user/features/composer/presentation/model/screen_display_mode.dart';
import 'package:tmail_ui_user/features/mailbox_dashboard/data/datasource/session_storage_composer_datasource.dart';
import 'package:tmail_ui_user/features/mailbox_dashboard/data/model/composer_cache.dart';
import 'package:tmail_ui_user/features/mailbox_dashboard/domain/repository/composer_cache_repository.dart';
@@ -10,17 +15,46 @@ class ComposerCacheRepositoryImpl extends ComposerCacheRepository {
ComposerCacheRepositoryImpl(this.composerCacheDataSource);
@override
ComposerCache getComposerCacheOnWeb() {
return composerCacheDataSource.getComposerCacheOnWeb();
Future<ComposerCache> getComposerCacheOnWeb(
AccountId accountId,
UserName userName
) {
return composerCacheDataSource.getComposerCacheOnWeb(accountId, userName);
}
@override
void removeComposerCacheOnWeb() {
Future<void> removeComposerCacheOnWeb() {
return composerCacheDataSource.removeComposerCacheOnWeb();
}
@override
void saveComposerCacheOnWeb(Email email) {
return composerCacheDataSource.saveComposerCacheOnWeb(email);
Future<void> saveComposerCacheOnWeb(
Email email,
{
required AccountId accountId,
required UserName userName,
required ScreenDisplayMode displayMode,
Identity? identity,
bool? readReceipentEnabled
}
) {
return composerCacheDataSource.saveComposerCacheOnWeb(
email,
accountId: accountId,
userName: userName,
displayMode: displayMode,
identity: identity,
readReceipentEnabled: readReceipentEnabled);
}
@override
Future<String> restoreEmailInlineImages(
String htmlContent,
TransformConfiguration transformConfiguration,
Map<String, String> mapUrlDownloadCID) {
return composerCacheDataSource.restoreEmailInlineImages(
htmlContent,
transformConfiguration,
mapUrlDownloadCID);
}
}
@@ -1,10 +1,31 @@
import 'package:core/presentation/utils/html_transformer/transform_configuration.dart';
import 'package:jmap_dart_client/jmap/account_id.dart';
import 'package:jmap_dart_client/jmap/core/user_name.dart';
import 'package:jmap_dart_client/jmap/identities/identity.dart';
import 'package:jmap_dart_client/jmap/mail/email/email.dart';
import 'package:tmail_ui_user/features/composer/presentation/model/screen_display_mode.dart';
import 'package:tmail_ui_user/features/mailbox_dashboard/data/model/composer_cache.dart';
abstract class ComposerCacheRepository {
void saveComposerCacheOnWeb(Email email);
Future<void> saveComposerCacheOnWeb(
Email email,
{
required AccountId accountId,
required UserName userName,
required ScreenDisplayMode displayMode,
Identity? identity,
bool? readReceipentEnabled
}
);
ComposerCache getComposerCacheOnWeb();
Future<ComposerCache> getComposerCacheOnWeb(
AccountId accountId,
UserName userName);
void removeComposerCacheOnWeb();
Future<void> removeComposerCacheOnWeb();
Future<String> restoreEmailInlineImages(
String htmlContent,
TransformConfiguration transformConfiguration,
Map<String, String> mapUrlDownloadCID);
}
@@ -1,6 +1,8 @@
import 'package:core/presentation/state/failure.dart';
import 'package:core/presentation/state/success.dart';
import 'package:dartz/dartz.dart';
import 'package:jmap_dart_client/jmap/account_id.dart';
import 'package:jmap_dart_client/jmap/core/user_name.dart';
import 'package:tmail_ui_user/features/mailbox_dashboard/domain/repository/composer_cache_repository.dart';
import 'package:tmail_ui_user/features/mailbox_dashboard/domain/state/get_composer_cache_state.dart';
@@ -9,12 +11,12 @@ class GetComposerCacheOnWebInteractor {
GetComposerCacheOnWebInteractor(this.composerCacheRepository);
Either<Failure, Success> execute() {
Stream<Either<Failure, Success>> execute(AccountId accountId, UserName userName) async* {
try {
final data = composerCacheRepository.getComposerCacheOnWeb();
return Right(GetComposerCacheSuccess(data));
final data = await composerCacheRepository.getComposerCacheOnWeb(accountId, userName);
yield Right(GetComposerCacheSuccess(data));
} catch (exception) {
return Left(GetComposerCacheFailure(exception));
yield Left(GetComposerCacheFailure(exception));
}
}
}
@@ -203,6 +203,17 @@ class UploadController extends BaseController {
_refreshListUploadAttachmentState();
}
void initializeUploadInlineAttachments(List<Attachment> inlineAttachments) {
final listUploadInlineImagesState = inlineAttachments
.map((inlineAttachment) => UploadFileState(
UploadTaskId(inlineAttachment.blobId!.value),
uploadStatus: UploadFileStatus.succeed,
attachment: inlineAttachment))
.toList();
_uploadingStateInlineFiles.addAll(listUploadInlineImagesState);
_refreshListUploadAttachmentState();
}
void deleteFileUploaded(UploadTaskId uploadId) {
_uploadingStateFiles.deleteElementByUploadTaskId(uploadId);
_refreshListUploadAttachmentState();