TF-2644 Handle total size exceeded maximum size when drag & drop multiple file to composer

This commit is contained in:
dab246
2024-03-01 17:09:09 +07:00
committed by Dat H. Pham
parent d646a5b9e4
commit 145a49d728
54 changed files with 1140 additions and 800 deletions
+1
View File
@@ -45,6 +45,7 @@ export 'utils/file_utils.dart';
export 'utils/option_param_mixin.dart'; export 'utils/option_param_mixin.dart';
export 'utils/print_utils.dart'; export 'utils/print_utils.dart';
export 'utils/broadcast_channel/broadcast_channel.dart'; export 'utils/broadcast_channel/broadcast_channel.dart';
export 'utils/list_utils.dart';
// Views // Views
export 'presentation/views/text/slogan_builder.dart'; export 'presentation/views/text/slogan_builder.dart';
+16
View File
@@ -0,0 +1,16 @@
import 'package:dartz/dartz.dart';
Tuple2<List<T>, List<T>> partition<T>(List<T> list, bool Function(T) predicate) {
List<T> trueList = [];
List<T> falseList = [];
for (var element in list) {
if (predicate(element)) {
trueList.add(element);
} else {
falseList.add(element);
}
}
return Tuple2(trueList, falseList);
}
@@ -10,15 +10,11 @@ class DownloadImageAsBase64Success extends UIState {
final String base64Uri; final String base64Uri;
final String cid; final String cid;
final FileInfo fileInfo; final FileInfo fileInfo;
final bool fromFileShared;
DownloadImageAsBase64Success( DownloadImageAsBase64Success(
this.base64Uri, this.base64Uri,
this.cid, this.cid,
this.fileInfo, this.fileInfo
{
this.fromFileShared = false
}
); );
@override @override
@@ -26,7 +22,6 @@ class DownloadImageAsBase64Success extends UIState {
base64Uri, base64Uri,
cid, cid,
fileInfo, fileInfo,
fromFileShared,
]; ];
} }
@@ -1,46 +1,25 @@
import 'package:core/presentation/state/failure.dart'; import 'package:core/presentation/state/failure.dart';
import 'package:core/presentation/state/success.dart'; import 'package:core/presentation/state/success.dart';
import 'package:model/upload/file_info.dart';
import 'package:tmail_ui_user/features/upload/domain/model/upload_attachment.dart'; import 'package:tmail_ui_user/features/upload/domain/model/upload_attachment.dart';
class UploadAttachmentSuccess extends UIState { class UploadAttachmentSuccess extends UIState {
final UploadAttachment uploadAttachment; final UploadAttachment uploadAttachment;
final bool isInline;
final bool fromFileShared;
UploadAttachmentSuccess( UploadAttachmentSuccess(this.uploadAttachment);
this.uploadAttachment,
{
this.isInline = false,
this.fromFileShared = false,
}
);
@override @override
List<Object?> get props => [ List<Object?> get props => [uploadAttachment];
uploadAttachment,
isInline,
fromFileShared,
];
} }
class UploadAttachmentFailure extends FeatureFailure { class UploadAttachmentFailure extends FeatureFailure {
final bool isInline;
final bool fromFileShared;
UploadAttachmentFailure( final FileInfo fileInfo;
dynamic exception,
{ UploadAttachmentFailure(dynamic exception, this.fileInfo) : super(exception: exception);
this.isInline = false,
this.fromFileShared = false,
}
) : super(exception: exception);
@override @override
List<Object?> get props => [ List<Object?> get props => [...super.props, fileInfo];
isInline,
fromFileShared,
...super.props
];
} }
@@ -16,7 +16,6 @@ class DownloadImageAsBase64Interactor {
{ {
double? maxWidth, double? maxWidth,
bool? compress, bool? compress,
bool fromFileShared = false,
} }
) async* { ) async* {
try { try {
@@ -33,7 +32,6 @@ class DownloadImageAsBase64Interactor {
result!, result!,
cid, cid,
fileInfo, fileInfo,
fromFileShared: fromFileShared
)); ));
} else { } else {
yield Left<Failure, Success>(DownloadImageAsBase64Failure(null)); yield Left<Failure, Success>(DownloadImageAsBase64Failure(null));
@@ -15,8 +15,6 @@ class UploadAttachmentInteractor {
FileInfo fileInfo, FileInfo fileInfo,
Uri uploadUri, { Uri uploadUri, {
CancelToken? cancelToken, CancelToken? cancelToken,
bool isInline = false,
bool fromFileShared = false,
}) async* { }) async* {
try { try {
final uploadAttachment = await _composerRepository.uploadAttachment( final uploadAttachment = await _composerRepository.uploadAttachment(
@@ -24,17 +22,9 @@ class UploadAttachmentInteractor {
uploadUri, uploadUri,
cancelToken: cancelToken cancelToken: cancelToken
); );
yield Right<Failure, Success>(UploadAttachmentSuccess( yield Right<Failure, Success>(UploadAttachmentSuccess(uploadAttachment));
uploadAttachment,
isInline: isInline,
fromFileShared: fromFileShared
));
} catch (e) { } catch (e) {
yield Left<Failure, Success>(UploadAttachmentFailure( yield Left<Failure, Success>(UploadAttachmentFailure(e, fileInfo));
e,
isInline: isInline,
fromFileShared: fromFileShared
));
} }
} }
} }
@@ -62,6 +62,7 @@ import 'package:tmail_ui_user/features/upload/data/datasource/attachment_upload_
import 'package:tmail_ui_user/features/upload/data/datasource_impl/attachment_upload_datasource_impl.dart'; import 'package:tmail_ui_user/features/upload/data/datasource_impl/attachment_upload_datasource_impl.dart';
import 'package:tmail_ui_user/features/upload/data/network/file_uploader.dart'; import 'package:tmail_ui_user/features/upload/data/network/file_uploader.dart';
import 'package:tmail_ui_user/features/upload/domain/usecases/local_file_picker_interactor.dart'; import 'package:tmail_ui_user/features/upload/domain/usecases/local_file_picker_interactor.dart';
import 'package:tmail_ui_user/features/upload/domain/usecases/local_image_picker_interactor.dart';
import 'package:tmail_ui_user/features/upload/presentation/controller/upload_controller.dart'; import 'package:tmail_ui_user/features/upload/presentation/controller/upload_controller.dart';
import 'package:tmail_ui_user/main/bindings/network/binding_tag.dart'; import 'package:tmail_ui_user/main/bindings/network/binding_tag.dart';
import 'package:tmail_ui_user/main/exceptions/cache_exception_thrower.dart'; import 'package:tmail_ui_user/main/exceptions/cache_exception_thrower.dart';
@@ -125,7 +126,7 @@ class ComposerBindings extends BaseBindings {
Get.find<FileUtils>(), Get.find<FileUtils>(),
Get.find<CacheExceptionThrower>())); Get.find<CacheExceptionThrower>()));
Get.lazyPut(() => RemoteServerSettingsDataSourceImpl( Get.lazyPut(() => RemoteServerSettingsDataSourceImpl(
Get.find<ServerSettingsAPI>(), Get.find<ServerSettingsAPI>(),
Get.find<RemoteExceptionThrower>())); Get.find<RemoteExceptionThrower>()));
} }
@@ -182,6 +183,7 @@ class ComposerBindings extends BaseBindings {
@override @override
void bindingsInteractor() { void bindingsInteractor() {
Get.lazyPut(() => LocalFilePickerInteractor()); Get.lazyPut(() => LocalFilePickerInteractor());
Get.lazyPut(() => LocalImagePickerInteractor());
Get.lazyPut(() => UploadAttachmentInteractor(Get.find<ComposerRepository>())); Get.lazyPut(() => UploadAttachmentInteractor(Get.find<ComposerRepository>()));
Get.lazyPut(() => SaveEmailAsDraftsInteractor( Get.lazyPut(() => SaveEmailAsDraftsInteractor(
Get.find<EmailRepository>(), Get.find<EmailRepository>(),
@@ -207,6 +209,7 @@ class ComposerBindings extends BaseBindings {
Get.lazyPut(() => ComposerController( Get.lazyPut(() => ComposerController(
Get.find<DeviceInfoPlugin>(), Get.find<DeviceInfoPlugin>(),
Get.find<LocalFilePickerInteractor>(), Get.find<LocalFilePickerInteractor>(),
Get.find<LocalImagePickerInteractor>(),
Get.find<GetEmailContentInteractor>(), Get.find<GetEmailContentInteractor>(),
Get.find<GetAllIdentitiesInteractor>(), Get.find<GetAllIdentitiesInteractor>(),
Get.find<UploadController>(), Get.find<UploadController>(),
@@ -1,11 +1,11 @@
import 'dart:async'; import 'dart:async';
import 'dart:io';
import 'dart:math'; import 'dart:math';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:core/core.dart'; import 'package:core/core.dart';
import 'package:dartz/dartz.dart'; import 'package:dartz/dartz.dart';
import 'package:desktop_drop/desktop_drop.dart';
import 'package:device_info_plus/device_info_plus.dart'; import 'package:device_info_plus/device_info_plus.dart';
import 'package:dropdown_button2/dropdown_button2.dart'; import 'package:dropdown_button2/dropdown_button2.dart';
import 'package:file_picker/file_picker.dart'; import 'package:file_picker/file_picker.dart';
@@ -51,8 +51,9 @@ import 'package:tmail_ui_user/features/composer/presentation/controller/rich_tex
import 'package:tmail_ui_user/features/composer/presentation/extensions/email_action_type_extension.dart'; import 'package:tmail_ui_user/features/composer/presentation/extensions/email_action_type_extension.dart';
import 'package:tmail_ui_user/features/composer/presentation/extensions/file_upload_extension.dart'; import 'package:tmail_ui_user/features/composer/presentation/extensions/file_upload_extension.dart';
import 'package:tmail_ui_user/features/composer/presentation/extensions/list_identities_extension.dart'; import 'package:tmail_ui_user/features/composer/presentation/extensions/list_identities_extension.dart';
import 'package:tmail_ui_user/features/composer/presentation/extensions/list_shared_media_file_extension.dart';
import 'package:tmail_ui_user/features/composer/presentation/mixin/drag_drog_file_mixin.dart';
import 'package:tmail_ui_user/features/composer/presentation/model/draggable_email_address.dart'; import 'package:tmail_ui_user/features/composer/presentation/model/draggable_email_address.dart';
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/inline_image.dart';
import 'package:tmail_ui_user/features/composer/presentation/model/prefix_recipient_state.dart'; import 'package:tmail_ui_user/features/composer/presentation/model/prefix_recipient_state.dart';
import 'package:tmail_ui_user/features/composer/presentation/model/save_to_draft_arguments.dart'; import 'package:tmail_ui_user/features/composer/presentation/model/save_to_draft_arguments.dart';
@@ -69,6 +70,7 @@ import 'package:tmail_ui_user/features/mailbox/domain/model/create_new_mailbox_r
import 'package:tmail_ui_user/features/mailbox_dashboard/domain/usecases/remove_composer_cache_on_web_interactor.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/domain/usecases/remove_composer_cache_on_web_interactor.dart';
import 'package:tmail_ui_user/features/mailbox_dashboard/domain/usecases/save_composer_cache_on_web_interactor.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/domain/usecases/save_composer_cache_on_web_interactor.dart';
import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart';
import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/model/draggable_app_state.dart';
import 'package:tmail_ui_user/features/manage_account/domain/state/get_all_identities_state.dart'; import 'package:tmail_ui_user/features/manage_account/domain/state/get_all_identities_state.dart';
import 'package:tmail_ui_user/features/manage_account/domain/usecases/get_all_identities_interactor.dart'; import 'package:tmail_ui_user/features/manage_account/domain/usecases/get_all_identities_interactor.dart';
import 'package:tmail_ui_user/features/manage_account/presentation/extensions/identity_extension.dart'; import 'package:tmail_ui_user/features/manage_account/presentation/extensions/identity_extension.dart';
@@ -79,18 +81,21 @@ import 'package:tmail_ui_user/features/sending_queue/domain/model/sending_email.
import 'package:tmail_ui_user/features/sending_queue/presentation/model/sending_email_arguments.dart'; import 'package:tmail_ui_user/features/sending_queue/presentation/model/sending_email_arguments.dart';
import 'package:tmail_ui_user/features/server_settings/domain/state/get_always_read_receipt_setting_state.dart'; import 'package:tmail_ui_user/features/server_settings/domain/state/get_always_read_receipt_setting_state.dart';
import 'package:tmail_ui_user/features/server_settings/domain/usecases/get_always_read_receipt_setting_interactor.dart'; import 'package:tmail_ui_user/features/server_settings/domain/usecases/get_always_read_receipt_setting_interactor.dart';
import 'package:tmail_ui_user/features/upload/domain/exceptions/pick_file_exception.dart';
import 'package:tmail_ui_user/features/upload/domain/extensions/list_file_info_extension.dart'; import 'package:tmail_ui_user/features/upload/domain/extensions/list_file_info_extension.dart';
import 'package:tmail_ui_user/features/upload/domain/extensions/list_file_info_extension.dart'; import 'package:tmail_ui_user/features/upload/domain/extensions/file_info_extension.dart';
import 'package:tmail_ui_user/features/upload/domain/model/upload_task_id.dart'; import 'package:tmail_ui_user/features/upload/domain/model/upload_task_id.dart';
import 'package:tmail_ui_user/features/upload/domain/state/attachment_upload_state.dart'; import 'package:tmail_ui_user/features/upload/domain/state/attachment_upload_state.dart';
import 'package:tmail_ui_user/features/upload/domain/state/local_file_picker_state.dart'; import 'package:tmail_ui_user/features/upload/domain/state/local_file_picker_state.dart';
import 'package:tmail_ui_user/features/upload/domain/state/local_image_picker_state.dart';
import 'package:tmail_ui_user/features/upload/domain/usecases/local_file_picker_interactor.dart'; import 'package:tmail_ui_user/features/upload/domain/usecases/local_file_picker_interactor.dart';
import 'package:tmail_ui_user/features/upload/domain/usecases/local_image_picker_interactor.dart';
import 'package:tmail_ui_user/features/upload/presentation/controller/upload_controller.dart'; import 'package:tmail_ui_user/features/upload/presentation/controller/upload_controller.dart';
import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; import 'package:tmail_ui_user/main/localizations/app_localizations.dart';
import 'package:tmail_ui_user/main/routes/route_navigation.dart'; import 'package:tmail_ui_user/main/routes/route_navigation.dart';
import 'package:universal_html/html.dart' as html; import 'package:universal_html/html.dart' as html;
class ComposerController extends BaseController { class ComposerController extends BaseController with DragDropFileMixin {
final mailboxDashBoardController = Get.find<MailboxDashBoardController>(); final mailboxDashBoardController = Get.find<MailboxDashBoardController>();
final richTextMobileTabletController = Get.find<RichTextMobileTabletController>(); final richTextMobileTabletController = Get.find<RichTextMobileTabletController>();
@@ -116,6 +121,7 @@ class ComposerController extends BaseController {
final listFromIdentities = RxList<Identity>(); final listFromIdentities = RxList<Identity>();
final LocalFilePickerInteractor _localFilePickerInteractor; final LocalFilePickerInteractor _localFilePickerInteractor;
final LocalImagePickerInteractor _localImagePickerInteractor;
final DeviceInfoPlugin _deviceInfoPlugin; final DeviceInfoPlugin _deviceInfoPlugin;
final GetEmailContentInteractor _getEmailContentInteractor; final GetEmailContentInteractor _getEmailContentInteractor;
final GetAllIdentitiesInteractor _getAllIdentitiesInteractor; final GetAllIdentitiesInteractor _getAllIdentitiesInteractor;
@@ -155,6 +161,12 @@ class ComposerController extends BaseController {
FocusNode? bccAddressFocusNode; FocusNode? bccAddressFocusNode;
FocusNode? searchIdentitiesFocusNode; FocusNode? searchIdentitiesFocusNode;
StreamSubscription<html.Event>? _subscriptionOnBeforeUnload;
StreamSubscription<html.Event>? _subscriptionOnDragEnter;
StreamSubscription<html.Event>? _subscriptionOnDragOver;
StreamSubscription<html.Event>? _subscriptionOnDragLeave;
StreamSubscription<html.Event>? _subscriptionOnDrop;
final RichTextController keyboardRichTextController = RichTextController(); final RichTextController keyboardRichTextController = RichTextController();
final ScrollController scrollController = ScrollController(); final ScrollController scrollController = ScrollController();
@@ -178,6 +190,7 @@ class ComposerController extends BaseController {
ComposerController( ComposerController(
this._deviceInfoPlugin, this._deviceInfoPlugin,
this._localFilePickerInteractor, this._localFilePickerInteractor,
this._localImagePickerInteractor,
this._getEmailContentInteractor, this._getEmailContentInteractor,
this._getAllIdentitiesInteractor, this._getAllIdentitiesInteractor,
this.uploadController, this.uploadController,
@@ -201,7 +214,7 @@ class ComposerController extends BaseController {
}); });
} else { } else {
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
_listenBrowserTabRefresh(); _listenBrowserEventAction();
}); });
} }
_getAlwaysReadReceiptSetting(); _getAlwaysReadReceiptSetting();
@@ -225,6 +238,11 @@ class ComposerController extends BaseController {
emailContentsViewState.value = Right(UIClosedState()); emailContentsViewState.value = Right(UIClosedState());
identitySelected.value = null; identitySelected.value = null;
listFromIdentities.clear(); listFromIdentities.clear();
_subscriptionOnBeforeUnload?.cancel();
_subscriptionOnDragEnter?.cancel();
_subscriptionOnDragOver?.cancel();
_subscriptionOnDragLeave?.cancel();
_subscriptionOnDrop?.cancel();
if (PlatformInfo.isMobile) { if (PlatformInfo.isMobile) {
FkUserAgent.release(); FkUserAgent.release();
} }
@@ -267,6 +285,8 @@ class ComposerController extends BaseController {
emailContentsViewState.value = Right(success); emailContentsViewState.value = Right(success);
} else if (success is LocalFilePickerSuccess) { } else if (success is LocalFilePickerSuccess) {
_handlePickFileSuccess(success); _handlePickFileSuccess(success);
} else if (success is LocalImagePickerSuccess) {
_handlePickImageSuccess(success);
} else if (success is GetEmailContentSuccess) { } else if (success is GetEmailContentSuccess) {
_getEmailContentSuccess(success); _getEmailContentSuccess(success);
} else if (success is GetEmailContentFromCacheSuccess) { } else if (success is GetEmailContentFromCacheSuccess) {
@@ -274,23 +294,11 @@ class ComposerController extends BaseController {
} else if (success is GetAllIdentitiesSuccess) { } else if (success is GetAllIdentitiesSuccess) {
_handleGetAllIdentitiesSuccess(success); _handleGetAllIdentitiesSuccess(success);
} else if (success is DownloadImageAsBase64Success) { } else if (success is DownloadImageAsBase64Success) {
final inlineImage = InlineImage(fileInfo: success.fileInfo, base64Uri: success.base64Uri);
if (PlatformInfo.isWeb) { if (PlatformInfo.isWeb) {
richTextWebController.insertImage( richTextWebController.insertImage(inlineImage);
InlineImage(
ImageSource.local,
fileInfo: success.fileInfo,
cid: success.cid,
base64Uri: success.base64Uri));
} else { } else {
richTextMobileTabletController.insertImage( richTextMobileTabletController.insertImage(inlineImage);
InlineImage(
ImageSource.local,
fileInfo: success.fileInfo,
cid: success.cid,
base64Uri: success.base64Uri
),
fromFileShare: success.fromFileShared
);
} }
maxWithEditor = null; maxWithEditor = null;
} else if (success is GetAlwaysReadReceiptSettingSuccess) { } else if (success is GetAlwaysReadReceiptSettingSuccess) {
@@ -301,8 +309,10 @@ class ComposerController extends BaseController {
@override @override
void handleFailureViewState(Failure failure) { void handleFailureViewState(Failure failure) {
super.handleFailureViewState(failure); super.handleFailureViewState(failure);
if (failure is LocalFilePickerFailure || failure is LocalFilePickerCancel) { if (failure is LocalFilePickerFailure) {
_pickFileFailure(failure); _handlePickFileFailure(failure);
} else if (failure is LocalImagePickerFailure) {
_handlePickImageFailure(failure);
} else if (failure is GetEmailContentFailure || } else if (failure is GetEmailContentFailure ||
failure is TransformHtmlEmailContentFailure) { failure is TransformHtmlEmailContentFailure) {
emailContentsViewState.value = Left(failure); emailContentsViewState.value = Left(failure);
@@ -358,8 +368,9 @@ class ComposerController extends BaseController {
}); });
} }
void _listenBrowserTabRefresh() { void _listenBrowserEventAction() {
html.window.onBeforeUnload.listen((event) async { log('ComposerController::_listenBrowserEventAction:');
_subscriptionOnBeforeUnload = html.window.onBeforeUnload.listen((event) async {
final userProfile = mailboxDashBoardController.userProfile.value; final userProfile = mailboxDashBoardController.userProfile.value;
_removeComposerCacheOnWebInteractor.execute(); _removeComposerCacheOnWebInteractor.execute();
if (userProfile != null) { if (userProfile != null) {
@@ -367,6 +378,22 @@ class ComposerController extends BaseController {
_saveComposerCacheOnWebInteractor.execute(draftEmail); _saveComposerCacheOnWebInteractor.execute(draftEmail);
} }
}); });
_subscriptionOnDragEnter = html.window.onDragEnter.listen((event) {
mailboxDashBoardController.localFileDraggableAppState.value = DraggableAppState.active;
});
_subscriptionOnDragOver = html.window.onDragOver.listen((event) {
mailboxDashBoardController.localFileDraggableAppState.value = DraggableAppState.active;
});
_subscriptionOnDragLeave = html.window.onDragLeave.listen((event) {
mailboxDashBoardController.localFileDraggableAppState.value = DraggableAppState.inActive;
});
_subscriptionOnDrop = html.window.onDrop.listen((event) {
mailboxDashBoardController.localFileDraggableAppState.value = DraggableAppState.inActive;
});
} }
void _scrollControllerEmailAddressListener() { void _scrollControllerEmailAddressListener() {
@@ -1064,29 +1091,45 @@ class ComposerController extends BaseController {
consumeState(_localFilePickerInteractor.execute(fileType: fileType)); consumeState(_localFilePickerInteractor.execute(fileType: fileType));
} }
void _pickFileFailure(Failure failure) { void _handlePickFileFailure(LocalFilePickerFailure failure) {
if (failure is LocalFilePickerFailure) { if (currentOverlayContext != null && currentContext != null && failure.exception is! PickFileCanceledException) {
if (currentOverlayContext != null && currentContext != null) { appToast.showToastErrorMessage(
appToast.showToastErrorMessage( currentOverlayContext!,
currentOverlayContext!, AppLocalizations.of(currentContext!).thisFileCannotBePicked);
AppLocalizations.of(currentContext!).can_not_upload_this_file_as_attachments); }
} }
void _handlePickImageFailure(LocalImagePickerFailure failure) {
if (currentOverlayContext != null && currentContext != null && failure.exception is! PickFileCanceledException) {
appToast.showToastErrorMessage(
currentOverlayContext!,
AppLocalizations.of(currentContext!).cannotSelectThisImage);
} }
} }
void _handlePickFileSuccess(LocalFilePickerSuccess success) { void _handlePickFileSuccess(LocalFilePickerSuccess success) {
uploadController.validateTotalSizeAttachmentsBeforeUpload( uploadController.validateTotalSizeAttachmentsBeforeUpload(
totalSizePreparedFiles: success.pickedFiles.totalSize, totalSizePreparedFiles: success.pickedFiles.totalSize,
callbackAction: () => _uploadAttachmentsAction(success.pickedFiles) onValidationSuccess: () => _uploadAttachmentsAction(pickedFiles: success.pickedFiles)
); );
} }
void _uploadAttachmentsAction(List<FileInfo> pickedFiles) { void _handlePickImageSuccess(LocalImagePickerSuccess success) {
uploadController.validateTotalSizeInlineAttachmentsBeforeUpload(
totalSizePreparedFiles: success.fileInfo.fileSize,
onValidationSuccess: () => _uploadAttachmentsAction(pickedFiles: [success.fileInfo.withInline()])
);
}
void _uploadAttachmentsAction({required List<FileInfo> pickedFiles}) {
final session = mailboxDashBoardController.sessionCurrent; final session = mailboxDashBoardController.sessionCurrent;
final accountId = mailboxDashBoardController.accountId.value; final accountId = mailboxDashBoardController.accountId.value;
if (session != null && accountId != null) { if (session != null && accountId != null) {
final uploadUri = session.getUploadUri(accountId, jmapUrl: _dynamicUrlInterceptors.jmapUrl); final uploadUri = session.getUploadUri(accountId, jmapUrl: _dynamicUrlInterceptors.jmapUrl);
uploadController.justUploadAttachmentsAction(pickedFiles, uploadUri); uploadController.justUploadAttachmentsAction(
uploadFiles: pickedFiles,
uploadUri: uploadUri,
);
} else { } else {
log('ComposerController::_uploadAttachmentsAction: SESSION OR ACCOUNT_ID is NULL'); log('ComposerController::_uploadAttachmentsAction: SESSION OR ACCOUNT_ID is NULL');
} }
@@ -1237,71 +1280,17 @@ class ComposerController extends BaseController {
); );
} }
File _covertSharedMediaFileToFile(SharedMediaFile sharedMediaFile) {
return File(
Platform.isIOS
? sharedMediaFile.type == SharedMediaType.FILE
? sharedMediaFile.path.toString().replaceAll('file:/', '').replaceAll('%20', ' ')
: sharedMediaFile.path.toString().replaceAll('%20', ' ')
: sharedMediaFile.path,
);
}
FileInfo _covertFileToFileInfo(File file) {
return FileInfo(
file.path.split('/').last,
file.path,
file.existsSync() ? file.lengthSync() : 0,
);
}
List<InlineImage> covertListSharedMediaFileToInlineImage(List<SharedMediaFile> value) {
List<File> newFiles = List.empty(growable: true);
if (value.isNotEmpty) {
for (var element in value) {
newFiles.add(_covertSharedMediaFileToFile(element));
}
}
final List<InlineImage> listInlineImage = newFiles.map(
(e) => InlineImage(
ImageSource.local,
fileInfo: _covertFileToFileInfo(e),
)
).toList();
return listInlineImage;
}
List<FileInfo> covertListSharedMediaFileToFileInfo(List<SharedMediaFile> value) {
List<File> newFiles = List.empty(growable: true);
if (value.isNotEmpty) {
for (var element in value) {
newFiles.add(_covertSharedMediaFileToFile(element));
}
}
final List<FileInfo> listFileInfo = newFiles.map(
(e) => _covertFileToFileInfo(e),
).toList();
return listFileInfo;
}
void _addAttachmentFromFileShare(List<SharedMediaFile> listSharedMediaFile) { void _addAttachmentFromFileShare(List<SharedMediaFile> listSharedMediaFile) {
final listImageSharedMediaFile = listSharedMediaFile.where((element) => element.type == SharedMediaType.IMAGE); final listFileInfo = listSharedMediaFile.toListFileInfo(isShared: true);
final listFileAttachmentSharedMediaFile = listSharedMediaFile.where((element) => element.type != SharedMediaType.IMAGE);
if (listImageSharedMediaFile.isNotEmpty) { final tupleListFileInfo = partition(listFileInfo, (fileInfo) => fileInfo.isInline == true);
final listInlineImage = covertListSharedMediaFileToInlineImage(listSharedMediaFile); final listAttachments = tupleListFileInfo.value2;
for (var e in listInlineImage) {
_uploadInlineAttachmentsAction(e.fileInfo!, fromFileShared: true); uploadController.validateTotalSizeAttachmentsBeforeUpload(
} totalSizePreparedFiles: listFileInfo.totalSize,
} totalSizePreparedFilesWithDispositionAttachment: listAttachments.totalSize,
if (listFileAttachmentSharedMediaFile.isNotEmpty) { onValidationSuccess: () => _uploadAttachmentsAction(pickedFiles: listFileInfo)
final listFileInfo = covertListSharedMediaFileToFileInfo(listSharedMediaFile); );
uploadController.validateTotalSizeAttachmentsBeforeUpload(
totalSizePreparedFiles: listFileInfo.totalSize,
callbackAction: () => _uploadAttachmentsAction(listFileInfo)
);
}
} }
void _getEmailContentFromSendingEmail(SendingEmail sendingEmail) { void _getEmailContentFromSendingEmail(SendingEmail sendingEmail) {
@@ -1712,80 +1701,8 @@ class ComposerController extends BaseController {
} else { } else {
maxWithEditor = maxWith - 120; maxWithEditor = maxWith - 120;
} }
final inlineImage = await _selectFromFile();
if (inlineImage != null) {
if (PlatformInfo.isWeb) {
_insertImageOnWeb(inlineImage);
} else {
_insertImageOnMobileAndTablet(inlineImage);
}
} else {
if (context.mounted) {
appToast.showToastErrorMessage(context, AppLocalizations.of(context).cannotSelectThisImage);
}
}
}
Future<InlineImage?> _selectFromFile() async { consumeState(_localImagePickerInteractor.execute());
final filePickerResult = await FilePicker.platform.pickFiles(
type: FileType.image,
withData: PlatformInfo.isWeb
);
if (filePickerResult?.files.isNotEmpty == true) {
PlatformFile platformFile = filePickerResult!.files.first;
final fileSelected = FileInfo(
platformFile.name,
PlatformInfo.isWeb ? '' : platformFile.path ?? '',
platformFile.size,
bytes: PlatformInfo.isWeb ? platformFile.bytes : null,
);
return InlineImage(ImageSource.local, fileInfo: fileSelected);
}
return null;
}
void _insertImageOnWeb(InlineImage inlineImage) {
if (inlineImage.source == ImageSource.local) {
_uploadInlineAttachmentsAction(inlineImage.fileInfo!);
} else {
richTextWebController.insertImage(inlineImage);
}
}
void _insertImageOnMobileAndTablet(InlineImage inlineImage) {
if (inlineImage.source == ImageSource.local) {
_uploadInlineAttachmentsAction(inlineImage.fileInfo!);
} else {
richTextMobileTabletController.insertImage(inlineImage);
}
}
void _uploadInlineAttachmentsAction(FileInfo pickedFile, {bool fromFileShared = false}) async {
if (!uploadController.isExceededMaxSizeAttachmentsPerEmail(totalSizePreparedFiles: pickedFile.fileSize)) {
final session = mailboxDashBoardController.sessionCurrent;
final accountId = mailboxDashBoardController.accountId.value;
if (session != null && accountId != null) {
final uploadUri = session.getUploadUri(accountId, jmapUrl: _dynamicUrlInterceptors.jmapUrl);
uploadController.uploadFileAction(
pickedFile,
uploadUri,
isInline: true,
fromFileShared: fromFileShared
);
}
} 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: () => {isSendEmailLoading.value = false},
title: AppLocalizations.of(currentContext!).maximum_files_size,
hasCancelButton: false);
}
}
} }
void _handleUploadInlineSuccess(SuccessAttachmentUploadState uploadState) { void _handleUploadInlineSuccess(SuccessAttachmentUploadState uploadState) {
@@ -1802,7 +1719,6 @@ class ComposerController extends BaseController {
uploadState.attachment.cid!, uploadState.attachment.cid!,
uploadState.fileInfo, uploadState.fileInfo,
maxWidth: maxWithEditor, maxWidth: maxWithEditor,
fromFileShared: uploadState.fromFileShared,
)); ));
} }
} }
@@ -1896,6 +1812,7 @@ class ComposerController extends BaseController {
log('ComposerController::handleInitHtmlEditorWeb:'); log('ComposerController::handleInitHtmlEditorWeb:');
_isEmailBodyLoaded = true; _isEmailBodyLoaded = true;
richTextWebController.editorController.setFullScreen(); richTextWebController.editorController.setFullScreen();
richTextWebController.editorController.setOnDragDropEvent();
onChangeTextEditorWeb(initContent); onChangeTextEditorWeb(initContent);
richTextWebController.setEnableCodeView(); richTextWebController.setEnableCodeView();
if (identitySelected.value == null) { if (identitySelected.value == null) {
@@ -1921,10 +1838,25 @@ class ComposerController extends BaseController {
void handleImageUploadSuccess ( void handleImageUploadSuccess (
BuildContext context, BuildContext context,
web_html_editor.FileUpload fileUpload List<web_html_editor.FileUpload> listFileUpload
) async { ) async {
log('ComposerController::handleImageUploadSuccess:NAME: ${fileUpload.name} | TYPE: ${fileUpload.type} | SIZE: ${fileUpload.size}'); log('ComposerController::handleImageUploadSuccess: COUNT_FILE_UPLOADED = ${listFileUpload.length}');
if (fileUpload.base64 == null) { List<FileInfo> listFileInfo = [];
await Future.forEach(listFileUpload, (fileUpload) async {
if (fileUpload.base64?.isNotEmpty == true) {
final fileInfo = await fileUpload.toFileInfo();
if (fileInfo != null) {
if (fileInfo.mimeType.startsWith(MediaTypeExtension.imageType) == true) {
listFileInfo.add(fileInfo.withInline());
} else {
listFileInfo.add(fileInfo);
}
}
}
});
if (listFileInfo.isEmpty && context.mounted) {
appToast.showToastErrorMessage( appToast.showToastErrorMessage(
context, context,
AppLocalizations.of(context).can_not_upload_this_file_as_attachments AppLocalizations.of(context).can_not_upload_this_file_as_attachments
@@ -1932,49 +1864,30 @@ class ComposerController extends BaseController {
return; return;
} }
if (fileUpload.type?.startsWith(Constant.imageType) == true) { final listAttachments = listFileInfo
final fileInfo = await fileUpload.toFileInfo(); .where((fileInfo) => fileInfo.isInline != true)
if (fileInfo != null) { .toList();
_uploadInlineAttachmentsAction(fileInfo);
} else if (context.mounted) { uploadController.validateTotalSizeAttachmentsBeforeUpload(
appToast.showToastErrorMessage( totalSizePreparedFiles: listFileInfo.totalSize,
context, totalSizePreparedFilesWithDispositionAttachment: listAttachments.totalSize,
AppLocalizations.of(context).can_not_upload_this_file_as_attachments onValidationSuccess: () => _uploadAttachmentsAction(pickedFiles: listFileInfo)
); );
}
} else {
final fileInfo = await fileUpload.toFileInfo();
if (fileInfo != null) {
_addAttachmentFromDragAndDrop(fileInfo: fileInfo);
} else if (context.mounted) {
appToast.showToastErrorMessage(
context,
AppLocalizations.of(context).can_not_upload_this_file_as_attachments
);
}
}
} }
void handleImageUploadFailure({ void handleImageUploadFailure({
required BuildContext context, required BuildContext context,
required web_html_editor.UploadError uploadError, required web_html_editor.UploadError uploadError,
web_html_editor.FileUpload? fileUpload, List<web_html_editor.FileUpload>? listFileUpload,
String? base64Str, String? base64Str,
}) { }) {
logError('ComposerController::handleImageUploadFailure:fileUpload: $fileUpload | uploadError: $uploadError'); logError('ComposerController::handleImageUploadFailure: COUNT_FILE_FAILED = ${listFileUpload?.length} | ERROR = $uploadError');
appToast.showToastErrorMessage( appToast.showToastErrorMessage(
context, context,
'${AppLocalizations.of(context).can_not_upload_this_file_as_attachments}. (${uploadError.name})' '${AppLocalizations.of(context).can_not_upload_this_file_as_attachments}. (${uploadError.name})'
); );
} }
void _addAttachmentFromDragAndDrop({required FileInfo fileInfo}) {
uploadController.validateTotalSizeAttachmentsBeforeUpload(
totalSizePreparedFiles: fileInfo.fileSize,
callbackAction: () => _uploadAttachmentsAction([fileInfo])
);
}
FocusNode? getNextFocusOfToEmailAddress() { FocusNode? getNextFocusOfToEmailAddress() {
if (ccRecipientState.value == PrefixRecipientState.enabled) { if (ccRecipientState.value == PrefixRecipientState.enabled) {
return ccAddressFocusNode; return ccAddressFocusNode;
@@ -2066,11 +1979,11 @@ class ComposerController extends BaseController {
_updateStatusEmailSendButton(); _updateStatusEmailSendButton();
} }
void addAttachmentWhenDragFromOtherEmail(Attachment attachment) { void onAttachmentDropZoneListener(Attachment attachment) {
log('ComposerController::addAttachmentWhenDragFromOtherEmail: attachment = $attachment'); log('ComposerController::onAttachmentDropZoneListener: attachment = $attachment');
uploadController.validateTotalSizeAttachmentsBeforeUpload( uploadController.validateTotalSizeAttachmentsBeforeUpload(
totalSizePreparedFiles: attachment.size?.value ?? 0, totalSizePreparedFiles: attachment.size?.value ?? 0,
callbackAction: () => uploadController.initializeUploadAttachments([attachment]) onValidationSuccess: () => uploadController.initializeUploadAttachments([attachment])
); );
} }
@@ -2144,4 +2057,33 @@ class ComposerController extends BaseController {
consumeState(_getAlwaysReadReceiptSettingInteractor.execute(accountId)); consumeState(_getAlwaysReadReceiptSettingInteractor.execute(accountId));
} }
} }
void handleOnDragEnterHtmlEditorWeb() {
mailboxDashBoardController.localFileDraggableAppState.value = DraggableAppState.active;
}
void onLocalFileDropZoneListener({
required BuildContext context,
required DropDoneDetails details
}) async {
final listFileInfo = await onDragDone(context: context, details: details);
if (listFileInfo.isEmpty && context.mounted) {
appToast.showToastErrorMessage(
context,
AppLocalizations.of(context).can_not_upload_this_file_as_attachments
);
return;
}
final listAttachments = listFileInfo
.where((fileInfo) => fileInfo.isInline != true)
.toList();
uploadController.validateTotalSizeAttachmentsBeforeUpload(
totalSizePreparedFiles: listFileInfo.totalSize,
totalSizePreparedFilesWithDispositionAttachment: listAttachments.totalSize,
onValidationSuccess: () => _uploadAttachmentsAction(pickedFiles: listFileInfo)
);
}
} }
@@ -17,10 +17,11 @@ import 'package:tmail_ui_user/features/composer/presentation/widgets/mobile/from
import 'package:tmail_ui_user/features/composer/presentation/widgets/recipient_composer_widget.dart'; import 'package:tmail_ui_user/features/composer/presentation/widgets/recipient_composer_widget.dart';
import 'package:tmail_ui_user/features/composer/presentation/widgets/subject_composer_widget.dart'; import 'package:tmail_ui_user/features/composer/presentation/widgets/subject_composer_widget.dart';
import 'package:tmail_ui_user/features/composer/presentation/widgets/web/attachment_composer_widget.dart'; import 'package:tmail_ui_user/features/composer/presentation/widgets/web/attachment_composer_widget.dart';
import 'package:tmail_ui_user/features/composer/presentation/widgets/web/attachment_drop_zone_widget.dart';
import 'package:tmail_ui_user/features/composer/presentation/widgets/web/bottom_bar_composer_widget.dart'; import 'package:tmail_ui_user/features/composer/presentation/widgets/web/bottom_bar_composer_widget.dart';
import 'package:tmail_ui_user/features/composer/presentation/widgets/web/desktop_app_bar_composer_widget.dart'; import 'package:tmail_ui_user/features/composer/presentation/widgets/web/desktop_app_bar_composer_widget.dart';
import 'package:tmail_ui_user/features/composer/presentation/widgets/web/drop_zone_widget.dart';
import 'package:tmail_ui_user/features/composer/presentation/widgets/web/from_composer_drop_down_widget.dart'; import 'package:tmail_ui_user/features/composer/presentation/widgets/web/from_composer_drop_down_widget.dart';
import 'package:tmail_ui_user/features/composer/presentation/widgets/web/local_file_drop_zone_widget.dart';
import 'package:tmail_ui_user/features/composer/presentation/widgets/web/mobile_responsive_app_bar_composer_widget.dart'; import 'package:tmail_ui_user/features/composer/presentation/widgets/web/mobile_responsive_app_bar_composer_widget.dart';
import 'package:tmail_ui_user/features/composer/presentation/widgets/web/toolbar_rich_text_builder.dart'; import 'package:tmail_ui_user/features/composer/presentation/widgets/web/toolbar_rich_text_builder.dart';
import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; import 'package:tmail_ui_user/main/localizations/app_localizations.dart';
@@ -157,72 +158,110 @@ class ComposerView extends GetWidget<ComposerController> {
margin: ComposerStyle.mobileSubjectMargin, margin: ComposerStyle.mobileSubjectMargin,
), ),
Expanded( Expanded(
child: Stack( child: LayoutBuilder(
children: [ builder: (context, constraintsEditor) {
Padding( return Stack(
padding: ComposerStyle.mobileEditorPadding, children: [
child: Obx(() => WebEditorView( Column(
editorController: controller.richTextWebController.editorController, children: [
arguments: controller.composerArguments.value, Expanded(
contentViewState: controller.emailContentsViewState.value, child: Padding(
currentWebContent: controller.textEditorWeb, padding: ComposerStyle.mobileEditorPadding,
onInitial: controller.handleInitHtmlEditorWeb, child: Obx(() => WebEditorView(
onChangeContent: controller.onChangeTextEditorWeb, editorController: controller.richTextWebController.editorController,
onFocus: controller.handleOnFocusHtmlEditorWeb, arguments: controller.composerArguments.value,
onUnFocus: controller.handleOnUnFocusHtmlEditorWeb, contentViewState: controller.emailContentsViewState.value,
onMouseDown: controller.handleOnMouseDownHtmlEditorWeb, currentWebContent: controller.textEditorWeb,
onEditorSettings: controller.richTextWebController.onEditorSettingsChange, onInitial: controller.handleInitHtmlEditorWeb,
onImageUploadSuccessAction: (fileUpload) => controller.handleImageUploadSuccess(context, fileUpload), onChangeContent: controller.onChangeTextEditorWeb,
onImageUploadFailureAction: (fileUpload, base64Str, uploadError) { onFocus: controller.handleOnFocusHtmlEditorWeb,
return controller.handleImageUploadFailure( onUnFocus: controller.handleOnUnFocusHtmlEditorWeb,
context: context, onMouseDown: controller.handleOnMouseDownHtmlEditorWeb,
uploadError: uploadError, onEditorSettings: controller.richTextWebController.onEditorSettingsChange,
fileUpload: fileUpload, onEditorTextSizeChanged: controller.richTextWebController.onEditorTextSizeChanged,
base64Str: base64Str, width: constraints.maxWidth,
); height: constraints.maxHeight,
}, onDragEnter: controller.handleOnDragEnterHtmlEditorWeb,
onEditorTextSizeChanged: controller.richTextWebController.onEditorTextSizeChanged, )),
width: constraints.maxWidth, ),
height: constraints.maxHeight, ),
)), Obx(() {
), if (controller.uploadController.listUploadAttachments.isNotEmpty) {
Align( return AttachmentComposerWidget(
alignment: AlignmentDirectional.topCenter, listFileUploaded: controller.uploadController.listUploadAttachments,
child: Obx(() => InsertImageLoadingBarWidget( isCollapsed: controller.isAttachmentCollapsed,
uploadInlineViewState: controller.uploadController.uploadInlineViewState.value, onDeleteAttachmentAction: (fileState) => controller.deleteAttachmentUploaded(fileState.uploadTaskId),
viewState: controller.viewState.value, onToggleExpandAttachmentAction: (isCollapsed) => controller.isAttachmentCollapsed = isCollapsed,
padding: ComposerStyle.insertImageLoadingBarPadding, );
)), } else {
), return const SizedBox.shrink();
], }
}),
Obx(() {
if (controller.richTextWebController.isFormattingOptionsEnabled) {
return ToolbarRichTextWebBuilder(
richTextWebController: controller.richTextWebController,
padding: ComposerStyle.richToolbarPadding,
decoration: const BoxDecoration(
color: ComposerStyle.richToolbarColor,
boxShadow: ComposerStyle.richToolbarShadow
),
);
} else {
return const SizedBox.shrink();
}
})
],
),
Align(
alignment: AlignmentDirectional.topCenter,
child: Obx(() => InsertImageLoadingBarWidget(
uploadInlineViewState: controller.uploadController.uploadInlineViewState.value,
viewState: controller.viewState.value,
padding: ComposerStyle.insertImageLoadingBarPadding,
)),
),
Obx(() {
if (controller.mailboxDashBoardController.isAttachmentDraggableAppActive) {
return Positioned.fill(
child: PointerInterceptor(
child: AttachmentDropZoneWidget(
imagePaths: controller.imagePaths,
width: constraintsEditor.maxWidth,
height: constraintsEditor.maxHeight,
onAttachmentDropZoneListener: controller.onAttachmentDropZoneListener,
)
),
);
} else {
return const SizedBox.shrink();
}
}),
Obx(() {
if (controller.mailboxDashBoardController.isLocalFileDraggableAppActive) {
return Positioned.fill(
child: PointerInterceptor(
child: LocalFileDropZoneWidget(
imagePaths: controller.imagePaths,
width: constraintsEditor.maxWidth,
height: constraintsEditor.maxHeight,
onLocalFileDropZoneListener: (details) =>
controller.onLocalFileDropZoneListener(
context: context,
details: details
),
)
),
);
} else {
return const SizedBox.shrink();
}
}),
],
);
}
), ),
), ),
Obx(() {
if (controller.uploadController.listUploadAttachments.isNotEmpty) {
return AttachmentComposerWidget(
listFileUploaded: controller.uploadController.listUploadAttachments,
isCollapsed: controller.isAttachmentCollapsed,
onDeleteAttachmentAction: (fileState) => controller.deleteAttachmentUploaded(fileState.uploadTaskId),
onToggleExpandAttachmentAction: (isCollapsed) => controller.isAttachmentCollapsed = isCollapsed,
);
} else {
return const SizedBox.shrink();
}
}),
Obx(() {
if (controller.richTextWebController.isFormattingOptionsEnabled) {
return ToolbarRichTextWebBuilder(
richTextWebController: controller.richTextWebController,
padding: ComposerStyle.richToolbarPadding,
decoration: const BoxDecoration(
color: ComposerStyle.richToolbarColor,
boxShadow: ComposerStyle.richToolbarShadow
),
);
} else {
return const SizedBox.shrink();
}
})
] ]
), ),
); );
@@ -336,123 +375,149 @@ class ComposerView extends GetWidget<ComposerController> {
margin: ComposerStyle.desktopSubjectMargin, margin: ComposerStyle.desktopSubjectMargin,
), ),
Expanded( Expanded(
child: Container( child: LayoutBuilder(
decoration: const BoxDecoration( builder: (context, constraintsEditor) {
border: Border( return Stack(
bottom: BorderSide( children: [
color: ComposerStyle.borderColor, Column(
width: 1 children: [
) Expanded(
), child: Container(
color: ComposerStyle.backgroundEditorColor
),
child: Stack(
children: [
Column(
children: [
Expanded(
child: Padding(
padding: ComposerStyle.desktopEditorPadding,
child: Obx(() {
return Stack(
children: [
WebEditorView(
editorController: controller.richTextWebController.editorController,
arguments: controller.composerArguments.value,
contentViewState: controller.emailContentsViewState.value,
currentWebContent: controller.textEditorWeb,
onInitial: controller.handleInitHtmlEditorWeb,
onChangeContent: controller.onChangeTextEditorWeb,
onFocus: controller.handleOnFocusHtmlEditorWeb,
onUnFocus: controller.handleOnUnFocusHtmlEditorWeb,
onMouseDown: controller.handleOnMouseDownHtmlEditorWeb,
onEditorSettings: controller.richTextWebController.onEditorSettingsChange,
onImageUploadSuccessAction: (fileUpload) => controller.handleImageUploadSuccess(context, fileUpload),
onImageUploadFailureAction: (fileUpload, base64Str, uploadError) {
return controller.handleImageUploadFailure(
context: context,
uploadError: uploadError,
fileUpload: fileUpload,
base64Str: base64Str,
);
},
onEditorTextSizeChanged: controller.richTextWebController.onEditorTextSizeChanged,
width: constraints.maxWidth,
height: constraints.maxHeight,
),
if (controller.mailboxDashBoardController.isDraggableAppActive)
PointerInterceptor(
child: DropZoneWidget(
width: constraints.maxWidth,
height: constraints.maxHeight,
addAttachmentFromDropZone: controller.addAttachmentWhenDragFromOtherEmail,
)
)
],
);
}),
),
),
Obx(() {
if (controller.uploadController.listUploadAttachments.isNotEmpty) {
return AttachmentComposerWidget(
listFileUploaded: controller.uploadController.listUploadAttachments,
isCollapsed: controller.isAttachmentCollapsed,
onDeleteAttachmentAction: (fileState) => controller.deleteAttachmentUploaded(fileState.uploadTaskId),
onToggleExpandAttachmentAction: (isCollapsed) => controller.isAttachmentCollapsed = isCollapsed,
);
} else {
return const SizedBox.shrink();
}
}),
Obx(() {
if (controller.richTextWebController.isFormattingOptionsEnabled) {
return ToolbarRichTextWebBuilder(
richTextWebController: controller.richTextWebController,
padding: ComposerStyle.richToolbarPadding,
decoration: const BoxDecoration( decoration: const BoxDecoration(
color: ComposerStyle.richToolbarColor, border: Border(
boxShadow: ComposerStyle.richToolbarShadow bottom: BorderSide(
color: ComposerStyle.borderColor,
width: 1
)
),
color: ComposerStyle.backgroundEditorColor
), ),
); child: Column(
} else { children: [
return const SizedBox.shrink(); Expanded(
} child: Padding(
}) padding: ComposerStyle.desktopEditorPadding,
], child: Obx(() {
), return WebEditorView(
Align( editorController: controller.richTextWebController.editorController,
alignment: AlignmentDirectional.topCenter, arguments: controller.composerArguments.value,
child: Obx(() => InsertImageLoadingBarWidget( contentViewState: controller.emailContentsViewState.value,
uploadInlineViewState: controller.uploadController.uploadInlineViewState.value, currentWebContent: controller.textEditorWeb,
viewState: controller.viewState.value, onInitial: controller.handleInitHtmlEditorWeb,
padding: ComposerStyle.insertImageLoadingBarPadding, onChangeContent: controller.onChangeTextEditorWeb,
)), onFocus: controller.handleOnFocusHtmlEditorWeb,
), onUnFocus: controller.handleOnUnFocusHtmlEditorWeb,
], onMouseDown: controller.handleOnMouseDownHtmlEditorWeb,
), onEditorSettings: controller.richTextWebController.onEditorSettingsChange,
onEditorTextSizeChanged: controller.richTextWebController.onEditorTextSizeChanged,
width: constraints.maxWidth,
height: constraints.maxHeight,
onDragEnter: controller.handleOnDragEnterHtmlEditorWeb,
);
}),
),
),
Obx(() {
if (controller.uploadController.listUploadAttachments.isNotEmpty) {
return AttachmentComposerWidget(
listFileUploaded: controller.uploadController.listUploadAttachments,
isCollapsed: controller.isAttachmentCollapsed,
onDeleteAttachmentAction: (fileState) => controller.deleteAttachmentUploaded(fileState.uploadTaskId),
onToggleExpandAttachmentAction: (isCollapsed) => controller.isAttachmentCollapsed = isCollapsed,
);
} else {
return const SizedBox.shrink();
}
}),
Obx(() {
if (controller.richTextWebController.isFormattingOptionsEnabled) {
return ToolbarRichTextWebBuilder(
richTextWebController: controller.richTextWebController,
padding: ComposerStyle.richToolbarPadding,
decoration: const BoxDecoration(
color: ComposerStyle.richToolbarColor,
boxShadow: ComposerStyle.richToolbarShadow
),
);
} else {
return const SizedBox.shrink();
}
})
],
),
),
),
Obx(() => BottomBarComposerWidget(
isCodeViewEnabled: controller.richTextWebController.codeViewEnabled,
isFormattingOptionsEnabled: controller.richTextWebController.isFormattingOptionsEnabled,
openRichToolbarAction: controller.richTextWebController.toggleFormattingOptions,
attachFileAction: () => controller.openFilePickerByType(context, FileType.any),
insertImageAction: () => controller.insertImage(context, constraints.maxWidth),
showCodeViewAction: controller.richTextWebController.toggleCodeView,
deleteComposerAction: () => controller.handleClickDeleteComposer(context),
saveToDraftAction: () => controller.saveToDraftAction(context),
sendMessageAction: () => controller.validateInformationBeforeSending(context),
requestReadReceiptAction: (position) {
controller.openPopupMenuAction(
context,
position,
_createReadReceiptPopupItems(context),
radius: ComposerStyle.popupMenuRadius
);
},
isSending: controller.isSendEmailLoading.value,
)),
],
),
Align(
alignment: AlignmentDirectional.topCenter,
child: Obx(() => InsertImageLoadingBarWidget(
uploadInlineViewState: controller.uploadController.uploadInlineViewState.value,
viewState: controller.viewState.value,
padding: ComposerStyle.insertImageLoadingBarPadding,
)),
),
Obx(() {
if (controller.mailboxDashBoardController.isAttachmentDraggableAppActive) {
return Positioned.fill(
child: PointerInterceptor(
child: AttachmentDropZoneWidget(
imagePaths: controller.imagePaths,
width: constraintsEditor.maxWidth,
height: constraintsEditor.maxHeight,
onAttachmentDropZoneListener: controller.onAttachmentDropZoneListener,
)
),
);
} else {
return const SizedBox.shrink();
}
}),
Obx(() {
if (controller.mailboxDashBoardController.isLocalFileDraggableAppActive) {
return Positioned.fill(
child: PointerInterceptor(
child: LocalFileDropZoneWidget(
imagePaths: controller.imagePaths,
width: constraintsEditor.maxWidth,
height: constraintsEditor.maxHeight,
onLocalFileDropZoneListener: (details) =>
controller.onLocalFileDropZoneListener(
context: context,
details: details
),
)
),
);
} else {
return const SizedBox.shrink();
}
}),
],
);
}
), ),
), ),
Obx(() => BottomBarComposerWidget(
isCodeViewEnabled: controller.richTextWebController.codeViewEnabled,
isFormattingOptionsEnabled: controller.richTextWebController.isFormattingOptionsEnabled,
openRichToolbarAction: controller.richTextWebController.toggleFormattingOptions,
attachFileAction: () => controller.openFilePickerByType(context, FileType.any),
insertImageAction: () => controller.insertImage(context, constraints.maxWidth),
showCodeViewAction: controller.richTextWebController.toggleCodeView,
deleteComposerAction: () => controller.handleClickDeleteComposer(context),
saveToDraftAction: () => controller.saveToDraftAction(context),
sendMessageAction: () => controller.validateInformationBeforeSending(context),
requestReadReceiptAction: (position) {
controller.openPopupMenuAction(
context,
position,
_createReadReceiptPopupItems(context),
radius: ComposerStyle.popupMenuRadius
);
},
isSending: controller.isSendEmailLoading.value,
)),
]), ]),
); );
}, },
@@ -567,87 +632,119 @@ class ComposerView extends GetWidget<ComposerController> {
margin: ComposerStyle.tabletSubjectMargin, margin: ComposerStyle.tabletSubjectMargin,
), ),
Expanded( Expanded(
child: Container( child: LayoutBuilder(
decoration: const BoxDecoration( builder: (context, constraintsEditor) {
border: Border( return Container(
bottom: BorderSide( decoration: const BoxDecoration(
color: ComposerStyle.borderColor, border: Border(
width: 1 bottom: BorderSide(
) color: ComposerStyle.borderColor,
), width: 1
color: ComposerStyle.backgroundEditorColor )
), ),
child: Stack( color: ComposerStyle.backgroundEditorColor
children: [ ),
Column( child: Stack(
children: [ children: [
Expanded( Column(
child: Padding( children: [
padding: ComposerStyle.tabletEditorPadding, Expanded(
child: Obx(() => WebEditorView( child: Padding(
editorController: controller.richTextWebController.editorController, padding: ComposerStyle.tabletEditorPadding,
arguments: controller.composerArguments.value, child: Obx(() => WebEditorView(
contentViewState: controller.emailContentsViewState.value, editorController: controller.richTextWebController.editorController,
currentWebContent: controller.textEditorWeb, arguments: controller.composerArguments.value,
onInitial: controller.handleInitHtmlEditorWeb, contentViewState: controller.emailContentsViewState.value,
onChangeContent: controller.onChangeTextEditorWeb, currentWebContent: controller.textEditorWeb,
onFocus: controller.handleOnFocusHtmlEditorWeb, onInitial: controller.handleInitHtmlEditorWeb,
onUnFocus: controller.handleOnUnFocusHtmlEditorWeb, onChangeContent: controller.onChangeTextEditorWeb,
onMouseDown: controller.handleOnMouseDownHtmlEditorWeb, onFocus: controller.handleOnFocusHtmlEditorWeb,
onEditorSettings: controller.richTextWebController.onEditorSettingsChange, onUnFocus: controller.handleOnUnFocusHtmlEditorWeb,
onImageUploadSuccessAction: (fileUpload) => controller.handleImageUploadSuccess(context, fileUpload), onMouseDown: controller.handleOnMouseDownHtmlEditorWeb,
onImageUploadFailureAction: (fileUpload, base64Str, uploadError) { onEditorSettings: controller.richTextWebController.onEditorSettingsChange,
return controller.handleImageUploadFailure( onEditorTextSizeChanged: controller.richTextWebController.onEditorTextSizeChanged,
context: context, width: constraints.maxWidth,
uploadError: uploadError, height: constraints.maxHeight,
fileUpload: fileUpload, onDragEnter: controller.handleOnDragEnterHtmlEditorWeb,
base64Str: base64Str, )),
),
),
Obx(() {
if (controller.uploadController.listUploadAttachments.isNotEmpty) {
return AttachmentComposerWidget(
listFileUploaded: controller.uploadController.listUploadAttachments,
isCollapsed: controller.isAttachmentCollapsed,
onDeleteAttachmentAction: (fileState) => controller.deleteAttachmentUploaded(fileState.uploadTaskId),
onToggleExpandAttachmentAction: (isCollapsed) => controller.isAttachmentCollapsed = isCollapsed,
); );
}, } else {
onEditorTextSizeChanged: controller.richTextWebController.onEditorTextSizeChanged, return const SizedBox.shrink();
width: constraints.maxWidth, }
height: constraints.maxHeight, }),
)), Obx(() {
), if (controller.richTextWebController.isFormattingOptionsEnabled) {
return ToolbarRichTextWebBuilder(
richTextWebController: controller.richTextWebController,
padding: ComposerStyle.richToolbarPadding,
decoration: const BoxDecoration(
color: ComposerStyle.richToolbarColor,
boxShadow: ComposerStyle.richToolbarShadow
),
);
} else {
return const SizedBox.shrink();
}
})
],
),
Align(
alignment: AlignmentDirectional.topCenter,
child: Obx(() => InsertImageLoadingBarWidget(
uploadInlineViewState: controller.uploadController.uploadInlineViewState.value,
viewState: controller.viewState.value,
padding: ComposerStyle.insertImageLoadingBarPadding,
)),
), ),
Obx(() { Obx(() {
if (controller.uploadController.listUploadAttachments.isNotEmpty) { if (controller.mailboxDashBoardController.isAttachmentDraggableAppActive) {
return AttachmentComposerWidget( return Positioned.fill(
listFileUploaded: controller.uploadController.listUploadAttachments, child: PointerInterceptor(
isCollapsed: controller.isAttachmentCollapsed, child: AttachmentDropZoneWidget(
onDeleteAttachmentAction: (fileState) => controller.deleteAttachmentUploaded(fileState.uploadTaskId), imagePaths: controller.imagePaths,
onToggleExpandAttachmentAction: (isCollapsed) => controller.isAttachmentCollapsed = isCollapsed, width: constraintsEditor.maxWidth,
height: constraintsEditor.maxHeight,
onAttachmentDropZoneListener: controller.onAttachmentDropZoneListener,
)
),
); );
} else { } else {
return const SizedBox.shrink(); return const SizedBox.shrink();
} }
}), }),
Obx(() { Obx(() {
if (controller.richTextWebController.isFormattingOptionsEnabled) { if (controller.mailboxDashBoardController.isLocalFileDraggableAppActive) {
return ToolbarRichTextWebBuilder( return Positioned.fill(
richTextWebController: controller.richTextWebController, child: PointerInterceptor(
padding: ComposerStyle.richToolbarPadding, child: LocalFileDropZoneWidget(
decoration: const BoxDecoration( imagePaths: controller.imagePaths,
color: ComposerStyle.richToolbarColor, width: constraintsEditor.maxWidth,
boxShadow: ComposerStyle.richToolbarShadow height: constraintsEditor.maxHeight,
onLocalFileDropZoneListener: (details) =>
controller.onLocalFileDropZoneListener(
context: context,
details: details
),
)
), ),
); );
} else { } else {
return const SizedBox.shrink(); return const SizedBox.shrink();
} }
}) }),
], ],
), ),
Align( );
alignment: AlignmentDirectional.topCenter, }
child: Obx(() => InsertImageLoadingBarWidget(
uploadInlineViewState: controller.uploadController.uploadInlineViewState.value,
viewState: controller.viewState.value,
padding: ComposerStyle.insertImageLoadingBarPadding,
)),
),
],
),
), ),
), ),
Obx(() => BottomBarComposerWidget( Obx(() => BottomBarComposerWidget(
@@ -6,27 +6,17 @@ import 'package:file_picker/file_picker.dart';
import 'package:rich_text_composer/rich_text_composer.dart'; import 'package:rich_text_composer/rich_text_composer.dart';
import 'package:tmail_ui_user/features/composer/presentation/controller/base_rich_text_controller.dart'; import 'package:tmail_ui_user/features/composer/presentation/controller/base_rich_text_controller.dart';
import 'package:tmail_ui_user/features/composer/presentation/model/header_style_type.dart'; import 'package:tmail_ui_user/features/composer/presentation/model/header_style_type.dart';
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/inline_image.dart';
class RichTextMobileTabletController extends BaseRichTextController { class RichTextMobileTabletController extends BaseRichTextController {
HtmlEditorApi? htmlEditorApi; HtmlEditorApi? htmlEditorApi;
void insertImage( void insertImage(InlineImage inlineImage) async {
InlineImage image, if (inlineImage.fileInfo.isShared == true) {
{ await htmlEditorApi?.moveCursorAtLastNode();
double? maxWithEditor,
bool fromFileShare = false
} }
) async { if (inlineImage.base64Uri?.isNotEmpty == true) {
log('RichTextMobileTabletController::insertImage(): $image | maxWithEditor: $maxWithEditor | $fromFileShare'); await htmlEditorApi?.insertHtml(inlineImage.base64Uri!);
if (image.source == ImageSource.network) {
htmlEditorApi?.insertImageLink(image.link!);
} else {
if (fromFileShare) {
await htmlEditorApi?.moveCursorAtLastNode();
}
await htmlEditorApi?.insertHtml(image.base64Uri ?? '');
} }
} }
@@ -15,7 +15,6 @@ import 'package:tmail_ui_user/features/composer/presentation/model/dropdown_menu
import 'package:tmail_ui_user/features/composer/presentation/model/font_name_type.dart'; import 'package:tmail_ui_user/features/composer/presentation/model/font_name_type.dart';
import 'package:tmail_ui_user/features/composer/presentation/model/formatting_options_state.dart'; import 'package:tmail_ui_user/features/composer/presentation/model/formatting_options_state.dart';
import 'package:tmail_ui_user/features/composer/presentation/model/header_style_type.dart'; import 'package:tmail_ui_user/features/composer/presentation/model/header_style_type.dart';
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/inline_image.dart';
import 'package:tmail_ui_user/features/composer/presentation/model/order_list_type.dart'; import 'package:tmail_ui_user/features/composer/presentation/model/order_list_type.dart';
import 'package:tmail_ui_user/features/composer/presentation/model/paragraph_type.dart'; import 'package:tmail_ui_user/features/composer/presentation/model/paragraph_type.dart';
@@ -194,13 +193,8 @@ class RichTextWebController extends BaseRichTextController {
bool isTextStyleTypeSelected(RichTextStyleType richTextStyleType) => bool isTextStyleTypeSelected(RichTextStyleType richTextStyleType) =>
listTextStyleApply.contains(richTextStyleType); listTextStyleApply.contains(richTextStyleType);
void insertImage(InlineImage image) async { void insertImage(InlineImage inlineImage) {
log('RichTextWebController::insertImage(): $image'); editorController.insertHtml("<div>${inlineImage.base64Uri ?? ''}</div><br>");
if (image.source == ImageSource.network) {
editorController.insertNetworkImage(image.link!);
} else {
editorController.insertHtml("<div>${image.base64Uri ?? ''}</div><br>");
}
} }
void applyNewFontStyle(FontNameType? newFont) { void applyNewFontStyle(FontNameType? newFont) {
@@ -0,0 +1,19 @@
import 'dart:io';
import 'package:model/upload/file_info.dart';
extension FileExtension on File {
FileInfo toFileInfo({
bool? isInline,
bool? isShared
}) {
return FileInfo(
fileName: path.split('/').last,
fileSize: existsSync() ? lengthSync() : 0,
filePath: path,
isInline: isInline,
isShared: isShared
);
}
}
@@ -29,7 +29,8 @@ extension FileUploadExtension on FileUpload {
return FileInfo.fromBytes( return FileInfo.fromBytes(
bytes: bytes, bytes: bytes,
name: name, name: name,
size: size size: size,
type: type,
); );
} else { } else {
return null; return null;
@@ -0,0 +1,8 @@
import 'package:model/upload/file_info.dart';
import 'package:receive_sharing_intent/receive_sharing_intent.dart';
import 'package:tmail_ui_user/features/composer/presentation/extensions/shared_media_file_extension.dart';
extension ListSharedMediaFileExtension on List<SharedMediaFile> {
List<FileInfo> toListFileInfo({bool? isShared}) => map((sharedMediaFile) => sharedMediaFile.toFileInfo(isShared: isShared)).toList();
}
@@ -0,0 +1,22 @@
import 'dart:io';
import 'package:core/utils/platform_info.dart';
import 'package:model/upload/file_info.dart';
import 'package:receive_sharing_intent/receive_sharing_intent.dart';
import 'package:tmail_ui_user/features/composer/presentation/extensions/file_extension.dart';
extension SharedMediaFileExtension on SharedMediaFile {
File toFile() {
if (PlatformInfo.isIOS) {
final pathFile = type == SharedMediaType.FILE
? path.toString().replaceAll('file:/', '').replaceAll('%20', ' ')
: path.toString().replaceAll('%20', ' ');
return File(pathFile);
} else {
return File(path);
}
}
FileInfo toFileInfo({bool? isShared}) => toFile().toFileInfo(isInline: type == SharedMediaType.IMAGE, isShared: isShared);
}
@@ -0,0 +1,52 @@
import 'dart:async' as async;
import 'package:async/async.dart';
import 'package:core/domain/extensions/media_type_extension.dart';
import 'package:desktop_drop/desktop_drop.dart';
import 'package:flutter/material.dart';
import 'package:future_loading_dialog/future_loading_dialog.dart';
import 'package:model/upload/file_info.dart';
import 'package:tmail_ui_user/main/localizations/app_localizations.dart';
mixin DragDropFileMixin {
async.Future<Result<T>> showFutureLoadingDialogFullScreen<T>({
required BuildContext context,
required async.Future<T> Function() future,
}) async {
return await showFutureLoadingDialog(
context: context,
title: AppLocalizations.of(context).loadingPleaseWait,
backLabel: AppLocalizations.of(context).close,
future: future,
);
}
async.Future<List<FileInfo>> onDragDone({
required BuildContext context,
required DropDoneDetails details
}) async {
final bytesList = await showFutureLoadingDialogFullScreen(
context: context,
future: () => async.Future.wait(
details.files.map(
(xFile) => xFile.readAsBytes(),
),
),
);
if (bytesList.error != null) return [];
final listFileInfo = <FileInfo>[];
for (var i = 0; i < bytesList.result!.length; i++) {
listFileInfo.add(
FileInfo(
bytes: bytesList.result![i],
fileName: details.files[i].name,
type: details.files[i].mimeType,
fileSize: bytesList.result![i].length,
isInline: details.files[i].mimeType?.startsWith(MediaTypeExtension.imageType) == true
),
);
}
return listFileInfo;
}
}
@@ -1,5 +0,0 @@
enum ImageSource {
local,
network
}
@@ -1,25 +1,19 @@
import 'package:equatable/equatable.dart'; import 'package:equatable/equatable.dart';
import 'package:model/model.dart'; import 'package:model/upload/file_info.dart';
import 'package:tmail_ui_user/features/composer/presentation/model/image_source.dart';
class InlineImage with EquatableMixin { class InlineImage with EquatableMixin {
final ImageSource source; final FileInfo fileInfo;
final String? link;
final FileInfo? fileInfo;
final String? cid;
final String? base64Uri; final String? base64Uri;
InlineImage( InlineImage({
this.source, required this.fileInfo,
{ this.base64Uri,
this.link, });
this.fileInfo,
this.cid,
this.base64Uri,
}
);
@override @override
List<Object?> get props => [source, link, fileInfo, cid, base64Uri]; List<Object?> get props => [
fileInfo,
base64Uri
];
} }
@@ -10,6 +10,7 @@ class BottomBarComposerWidgetStyle {
static const double richTextIconSize = 24; static const double richTextIconSize = 24;
static const double sendButtonRadius = 8; static const double sendButtonRadius = 8;
static const double sendButtonIconSpace = 5; static const double sendButtonIconSpace = 5;
static const double height = 60;
static const Color backgroundColor = Colors.white; static const Color backgroundColor = Colors.white;
static const Color iconColor = AppColor.colorRichButtonComposer; static const Color iconColor = AppColor.colorRichButtonComposer;
@@ -17,7 +18,7 @@ class BottomBarComposerWidgetStyle {
static const Color selectedBackgroundColor = AppColor.colorSelected; static const Color selectedBackgroundColor = AppColor.colorSelected;
static const Color selectedIconColor = AppColor.primaryColor; static const Color selectedIconColor = AppColor.primaryColor;
static const EdgeInsetsGeometry padding = EdgeInsetsDirectional.symmetric(horizontal: 32, vertical: 12); static const EdgeInsetsGeometry padding = EdgeInsetsDirectional.symmetric(horizontal: 32);
static const EdgeInsetsGeometry iconPadding = EdgeInsetsDirectional.all(5); static const EdgeInsetsGeometry iconPadding = EdgeInsetsDirectional.all(5);
static const EdgeInsetsGeometry sendButtonPadding = EdgeInsetsDirectional.symmetric(vertical: 8, horizontal: 24); static const EdgeInsetsGeometry sendButtonPadding = EdgeInsetsDirectional.symmetric(vertical: 8, horizontal: 24);
static const EdgeInsetsGeometry richTextIconPadding = EdgeInsetsDirectional.all(2); static const EdgeInsetsGeometry richTextIconPadding = EdgeInsetsDirectional.all(2);
@@ -1,5 +1,6 @@
import 'package:core/presentation/extensions/color_extension.dart'; import 'package:core/presentation/extensions/color_extension.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:tmail_ui_user/features/composer/presentation/styles/web/bottom_bar_composer_widget_style.dart';
class DropZoneWidgetStyle { class DropZoneWidgetStyle {
static const double space = 20; static const double space = 20;
@@ -8,11 +9,16 @@ class DropZoneWidgetStyle {
static const List<double> dashSize = [6, 3]; static const List<double> dashSize = [6, 3];
static const Color backgroundColor = AppColor.colorDropZoneBackground; static Color backgroundColor = AppColor.colorDropZoneBackground.withOpacity(0.7);
static const Color borderColor = AppColor.colorDropZoneBorder; static const Color borderColor = AppColor.colorDropZoneBorder;
static const EdgeInsetsGeometry padding = EdgeInsets.all(20); static const EdgeInsetsGeometry padding = EdgeInsets.all(20);
static const EdgeInsetsGeometry margin = EdgeInsetsDirectional.symmetric(vertical: 8); static const EdgeInsetsGeometry margin = EdgeInsetsDirectional.only(
bottom: BottomBarComposerWidgetStyle.height,
start: 8,
end: 8,
top: 8
);
static const TextStyle labelTextStyle = TextStyle( static const TextStyle labelTextStyle = TextStyle(
color: Colors.black, color: Colors.black,
@@ -26,11 +26,10 @@ class WebEditorView extends StatelessWidget with EditorViewMixin {
final VoidCallback? onUnFocus; final VoidCallback? onUnFocus;
final OnMouseDownEditorAction? onMouseDown; final OnMouseDownEditorAction? onMouseDown;
final OnEditorSettingsChange? onEditorSettings; final OnEditorSettingsChange? onEditorSettings;
final OnImageUploadSuccessAction? onImageUploadSuccessAction;
final OnImageUploadFailureAction? onImageUploadFailureAction;
final OnEditorTextSizeChanged? onEditorTextSizeChanged; final OnEditorTextSizeChanged? onEditorTextSizeChanged;
final double? width; final double? width;
final double? height; final double? height;
final VoidCallback? onDragEnter;
const WebEditorView({ const WebEditorView({
super.key, super.key,
@@ -44,11 +43,10 @@ class WebEditorView extends StatelessWidget with EditorViewMixin {
this.onUnFocus, this.onUnFocus,
this.onMouseDown, this.onMouseDown,
this.onEditorSettings, this.onEditorSettings,
this.onImageUploadSuccessAction,
this.onImageUploadFailureAction,
this.onEditorTextSizeChanged, this.onEditorTextSizeChanged,
this.width, this.width,
this.height, this.height,
this.onDragEnter,
}); });
@override @override
@@ -71,11 +69,10 @@ class WebEditorView extends StatelessWidget with EditorViewMixin {
onUnFocus: onUnFocus, onUnFocus: onUnFocus,
onMouseDown: onMouseDown, onMouseDown: onMouseDown,
onEditorSettings: onEditorSettings, onEditorSettings: onEditorSettings,
onImageUploadSuccessAction: onImageUploadSuccessAction,
onImageUploadFailureAction: onImageUploadFailureAction,
onEditorTextSizeChanged: onEditorTextSizeChanged, onEditorTextSizeChanged: onEditorTextSizeChanged,
width: width, width: width,
height: height, height: height,
onDragEnter: onDragEnter,
); );
case EmailActionType.editDraft: case EmailActionType.editDraft:
case EmailActionType.editSendingEmail: case EmailActionType.editSendingEmail:
@@ -97,11 +94,10 @@ class WebEditorView extends StatelessWidget with EditorViewMixin {
onUnFocus: onUnFocus, onUnFocus: onUnFocus,
onMouseDown: onMouseDown, onMouseDown: onMouseDown,
onEditorSettings: onEditorSettings, onEditorSettings: onEditorSettings,
onImageUploadSuccessAction: onImageUploadSuccessAction,
onImageUploadFailureAction: onImageUploadFailureAction,
onEditorTextSizeChanged: onEditorTextSizeChanged, onEditorTextSizeChanged: onEditorTextSizeChanged,
width: width, width: width,
height: height, height: height,
onDragEnter: onDragEnter,
), ),
(success) { (success) {
if (success is GetEmailContentLoading) { if (success is GetEmailContentLoading) {
@@ -123,11 +119,10 @@ class WebEditorView extends StatelessWidget with EditorViewMixin {
onUnFocus: onUnFocus, onUnFocus: onUnFocus,
onMouseDown: onMouseDown, onMouseDown: onMouseDown,
onEditorSettings: onEditorSettings, onEditorSettings: onEditorSettings,
onImageUploadSuccessAction: onImageUploadSuccessAction,
onImageUploadFailureAction: onImageUploadFailureAction,
onEditorTextSizeChanged: onEditorTextSizeChanged, onEditorTextSizeChanged: onEditorTextSizeChanged,
width: width, width: width,
height: height, height: height,
onDragEnter: onDragEnter,
); );
} }
} }
@@ -156,11 +151,10 @@ class WebEditorView extends StatelessWidget with EditorViewMixin {
onUnFocus: onUnFocus, onUnFocus: onUnFocus,
onMouseDown: onMouseDown, onMouseDown: onMouseDown,
onEditorSettings: onEditorSettings, onEditorSettings: onEditorSettings,
onImageUploadSuccessAction: onImageUploadSuccessAction,
onImageUploadFailureAction: onImageUploadFailureAction,
onEditorTextSizeChanged: onEditorTextSizeChanged, onEditorTextSizeChanged: onEditorTextSizeChanged,
width: width, width: width,
height: height, height: height,
onDragEnter: onDragEnter,
); );
}, },
(success) { (success) {
@@ -185,11 +179,10 @@ class WebEditorView extends StatelessWidget with EditorViewMixin {
onUnFocus: onUnFocus, onUnFocus: onUnFocus,
onMouseDown: onMouseDown, onMouseDown: onMouseDown,
onEditorSettings: onEditorSettings, onEditorSettings: onEditorSettings,
onImageUploadSuccessAction: onImageUploadSuccessAction,
onImageUploadFailureAction: onImageUploadFailureAction,
onEditorTextSizeChanged: onEditorTextSizeChanged, onEditorTextSizeChanged: onEditorTextSizeChanged,
width: width, width: width,
height: height, height: height,
onDragEnter: onDragEnter,
); );
} }
} }
@@ -205,11 +198,10 @@ class WebEditorView extends StatelessWidget with EditorViewMixin {
onUnFocus: onUnFocus, onUnFocus: onUnFocus,
onMouseDown: onMouseDown, onMouseDown: onMouseDown,
onEditorSettings: onEditorSettings, onEditorSettings: onEditorSettings,
onImageUploadSuccessAction: onImageUploadSuccessAction,
onImageUploadFailureAction: onImageUploadFailureAction,
onEditorTextSizeChanged: onEditorTextSizeChanged, onEditorTextSizeChanged: onEditorTextSizeChanged,
width: width, width: width,
height: height, height: height,
onDragEnter: onDragEnter,
); );
} }
} }
@@ -79,7 +79,7 @@ class AttachmentHeaderComposerWidget extends StatelessWidget {
margin: const EdgeInsetsDirectional.only(start: 4), margin: const EdgeInsetsDirectional.only(start: 4),
padding: const EdgeInsets.all(3), padding: const EdgeInsets.all(3),
iconColor: AppColor.colorBackgroundQuotasWarning, iconColor: AppColor.colorBackgroundQuotasWarning,
tooltipMessage: AppLocalizations.of(context).messageWarningDialogWhenExceedMaximumFileSizeComposer, tooltipMessage: AppLocalizations.of(context).warningMessageWhenExceedGenerallySizeInComposer,
backgroundColor: Colors.transparent, backgroundColor: Colors.transparent,
) )
], ],
@@ -89,5 +89,5 @@ class AttachmentHeaderComposerWidget extends StatelessWidget {
} }
bool get _isExceedMaximumSizeFileAttachedInComposer => bool get _isExceedMaximumSizeFileAttachedInComposer =>
listFileUploaded.totalSize > AppConfig.maximumMegabytesSizeFileAttachedInComposer * 1024 * 1024; listFileUploaded.totalSize > AppConfig.warningAttachmentFileSizeInMegabytes * 1024 * 1024;
} }
@@ -3,42 +3,35 @@ import 'package:core/presentation/resources/image_paths.dart';
import 'package:dotted_border/dotted_border.dart'; import 'package:dotted_border/dotted_border.dart';
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:flutter_svg/flutter_svg.dart'; import 'package:flutter_svg/flutter_svg.dart';
import 'package:get/get.dart';
import 'package:model/email/attachment.dart'; import 'package:model/email/attachment.dart';
import 'package:tmail_ui_user/features/composer/presentation/styles/web/drop_zone_widget_style.dart'; import 'package:tmail_ui_user/features/composer/presentation/styles/web/drop_zone_widget_style.dart';
import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; import 'package:tmail_ui_user/main/localizations/app_localizations.dart';
typedef OnAddAttachmentFromDropZone = Function(Attachment attachment); typedef OnAttachmentDropZoneListener = Function(Attachment attachment);
class DropZoneWidget extends StatefulWidget { class AttachmentDropZoneWidget extends StatelessWidget {
final ImagePaths imagePaths;
final double? width; final double? width;
final double? height; final double? height;
final OnAddAttachmentFromDropZone? addAttachmentFromDropZone; final OnAttachmentDropZoneListener? onAttachmentDropZoneListener;
const DropZoneWidget({ const AttachmentDropZoneWidget({
super.key, super.key,
required this.imagePaths,
this.width, this.width,
this.height, this.height,
this.addAttachmentFromDropZone this.onAttachmentDropZoneListener
}); });
@override
State<DropZoneWidget> createState() => _DropZoneWidgetState();
}
class _DropZoneWidgetState extends State<DropZoneWidget> {
final _imagePaths = Get.find<ImagePaths>();
bool _isDragging = false;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return DragTarget<Attachment>( return DragTarget<Attachment>(
builder: (context, candidateData, rejectedData) { builder: (context, _, __) {
if (_isDragging) { return SizedBox(
return Padding( width: width,
height: height,
child: Padding(
padding: DropZoneWidgetStyle.margin, padding: DropZoneWidgetStyle.margin,
child: DottedBorder( child: DottedBorder(
borderType: BorderType.RRect, borderType: BorderType.RRect,
@@ -48,20 +41,18 @@ class _DropZoneWidgetState extends State<DropZoneWidget> {
dashPattern: DropZoneWidgetStyle.dashSize, dashPattern: DropZoneWidgetStyle.dashSize,
child: Container( child: Container(
clipBehavior: Clip.antiAlias, clipBehavior: Clip.antiAlias,
decoration: const ShapeDecoration( decoration: ShapeDecoration(
color: DropZoneWidgetStyle.backgroundColor, color: DropZoneWidgetStyle.backgroundColor,
shape: RoundedRectangleBorder( shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(DropZoneWidgetStyle.radius)), borderRadius: BorderRadius.all(Radius.circular(DropZoneWidgetStyle.radius)),
), ),
), ),
width: widget.width,
height: widget.height,
padding: DropZoneWidgetStyle.padding, padding: DropZoneWidgetStyle.padding,
alignment: AlignmentDirectional.center, alignment: AlignmentDirectional.center,
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
SvgPicture.asset(_imagePaths.icDropZoneIcon), SvgPicture.asset(imagePaths.icDropZoneIcon),
const SizedBox(height: DropZoneWidgetStyle.space), const SizedBox(height: DropZoneWidgetStyle.space),
Text( Text(
AppLocalizations.of(context).dropFileHereToAttachThem, AppLocalizations.of(context).dropFileHereToAttachThem,
@@ -71,22 +62,10 @@ class _DropZoneWidgetState extends State<DropZoneWidget> {
), ),
), ),
), ),
); ),
} else { );
return SizedBox(width: widget.width, height: widget.height);
}
},
onAccept: widget.addAttachmentFromDropZone,
onLeave: (attachment) {
if (_isDragging) {
setState(() => _isDragging = false);
}
},
onMove: (details) {
if (!_isDragging) {
setState(() => _isDragging = true);
}
}, },
onAccept: onAttachmentDropZoneListener
); );
} }
} }
@@ -42,6 +42,7 @@ class BottomBarComposerWidget extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Container( return Container(
padding: BottomBarComposerWidgetStyle.padding, padding: BottomBarComposerWidgetStyle.padding,
height: BottomBarComposerWidgetStyle.height,
color: BottomBarComposerWidgetStyle.backgroundColor, color: BottomBarComposerWidgetStyle.backgroundColor,
child: Row( child: Row(
children: [ children: [
@@ -0,0 +1,69 @@
import 'package:core/presentation/resources/image_paths.dart';
import 'package:dotted_border/dotted_border.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:tmail_ui_user/features/composer/presentation/styles/web/drop_zone_widget_style.dart';
import 'package:tmail_ui_user/main/localizations/app_localizations.dart';
import 'package:desktop_drop/desktop_drop.dart';
typedef OnLocalFileDropZoneListener = Function(DropDoneDetails details);
class LocalFileDropZoneWidget extends StatelessWidget {
final ImagePaths imagePaths;
final double? width;
final double? height;
final OnLocalFileDropZoneListener? onLocalFileDropZoneListener;
const LocalFileDropZoneWidget({
super.key,
required this.imagePaths,
this.width,
this.height,
this.onLocalFileDropZoneListener
});
@override
Widget build(BuildContext context) {
return DropTarget(
onDragDone: onLocalFileDropZoneListener,
child: SizedBox(
width: width,
height: height,
child: Padding(
padding: DropZoneWidgetStyle.margin,
child: DottedBorder(
borderType: BorderType.RRect,
radius: const Radius.circular(DropZoneWidgetStyle.radius),
color: DropZoneWidgetStyle.borderColor,
strokeWidth: DropZoneWidgetStyle.borderWidth,
dashPattern: DropZoneWidgetStyle.dashSize,
child: Container(
clipBehavior: Clip.antiAlias,
decoration: ShapeDecoration(
color: DropZoneWidgetStyle.backgroundColor,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(DropZoneWidgetStyle.radius)),
),
),
padding: DropZoneWidgetStyle.padding,
alignment: AlignmentDirectional.center,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
SvgPicture.asset(imagePaths.icDropZoneIcon),
const SizedBox(height: DropZoneWidgetStyle.space),
Text(
AppLocalizations.of(context).dropFileHereToAttachThem,
style: DropZoneWidgetStyle.labelTextStyle,
)
]
),
),
),
),
),
);
}
}
@@ -11,8 +11,6 @@ typedef OnChangeContentEditorAction = Function(String? text);
typedef OnInitialContentEditorAction = Function(String text); typedef OnInitialContentEditorAction = Function(String text);
typedef OnMouseDownEditorAction = Function(BuildContext context); typedef OnMouseDownEditorAction = Function(BuildContext context);
typedef OnEditorSettingsChange = Function(EditorSettings settings); typedef OnEditorSettingsChange = Function(EditorSettings settings);
typedef OnImageUploadSuccessAction = Function(FileUpload fileUpload);
typedef OnImageUploadFailureAction = Function(FileUpload? fileUpload, String? base64Str, UploadError error);
typedef OnEditorTextSizeChanged = Function(int? size); typedef OnEditorTextSizeChanged = Function(int? size);
class WebEditorWidget extends StatefulWidget { class WebEditorWidget extends StatefulWidget {
@@ -26,11 +24,10 @@ class WebEditorWidget extends StatefulWidget {
final VoidCallback? onUnFocus; final VoidCallback? onUnFocus;
final OnMouseDownEditorAction? onMouseDown; final OnMouseDownEditorAction? onMouseDown;
final OnEditorSettingsChange? onEditorSettings; final OnEditorSettingsChange? onEditorSettings;
final OnImageUploadSuccessAction? onImageUploadSuccessAction;
final OnImageUploadFailureAction? onImageUploadFailureAction;
final OnEditorTextSizeChanged? onEditorTextSizeChanged; final OnEditorTextSizeChanged? onEditorTextSizeChanged;
final double? width; final double? width;
final double? height; final double? height;
final VoidCallback? onDragEnter;
const WebEditorWidget({ const WebEditorWidget({
super.key, super.key,
@@ -43,11 +40,10 @@ class WebEditorWidget extends StatefulWidget {
this.onUnFocus, this.onUnFocus,
this.onMouseDown, this.onMouseDown,
this.onEditorSettings, this.onEditorSettings,
this.onImageUploadSuccessAction,
this.onImageUploadFailureAction,
this.onEditorTextSizeChanged, this.onEditorTextSizeChanged,
this.width, this.width,
this.height, this.height,
this.onDragEnter,
}); });
@override @override
@@ -133,6 +129,7 @@ class _WebEditorState extends State<WebEditorWidget> {
initialText: widget.content, initialText: widget.content,
customBodyCssStyle: HtmlUtils.customCssStyleHtmlEditor(direction: widget.direction), customBodyCssStyle: HtmlUtils.customCssStyleHtmlEditor(direction: widget.direction),
spellCheck: true, spellCheck: true,
disableDragAndDrop: true,
webInitialScripts: UnmodifiableListView([ webInitialScripts: UnmodifiableListView([
WebScript( WebScript(
name: HtmlUtils.lineHeight100Percent.name, name: HtmlUtils.lineHeight100Percent.name,
@@ -145,7 +142,7 @@ class _WebEditorState extends State<WebEditorWidget> {
WebScript( WebScript(
name: HtmlUtils.unregisterDropListener.name, name: HtmlUtils.unregisterDropListener.name,
script: HtmlUtils.unregisterDropListener.script, script: HtmlUtils.unregisterDropListener.script,
), )
]) ])
), ),
htmlToolbarOptions: const HtmlToolbarOptions( htmlToolbarOptions: const HtmlToolbarOptions(
@@ -154,8 +151,8 @@ class _WebEditorState extends State<WebEditorWidget> {
), ),
otherOptions: OtherOptions( otherOptions: OtherOptions(
height: height, height: height,
dropZoneWidth: dropZoneWidth, // dropZoneWidth: dropZoneWidth,
dropZoneHeight: dropZoneHeight, // dropZoneHeight: dropZoneHeight,
), ),
callbacks: Callbacks( callbacks: Callbacks(
onBeforeCommand: widget.onChangeContent, onBeforeCommand: widget.onChangeContent,
@@ -173,12 +170,12 @@ class _WebEditorState extends State<WebEditorWidget> {
onMouseDown: () => widget.onMouseDown?.call(context), onMouseDown: () => widget.onMouseDown?.call(context),
onChangeSelection: widget.onEditorSettings, onChangeSelection: widget.onEditorSettings,
onChangeCodeview: widget.onChangeContent, onChangeCodeview: widget.onChangeContent,
onImageUpload: widget.onImageUploadSuccessAction,
onImageUploadError: widget.onImageUploadFailureAction,
onTextFontSizeChanged: widget.onEditorTextSizeChanged, onTextFontSizeChanged: widget.onEditorTextSizeChanged,
onPaste: () => _editorController.evaluateJavascriptWeb( onPaste: () => _editorController.evaluateJavascriptWeb(
HtmlUtils.lineHeight100Percent.name HtmlUtils.lineHeight100Percent.name
), ),
onDragEnter: widget.onDragEnter,
onDragLeave: () {},
), ),
); );
} }
@@ -339,9 +339,9 @@ class EmailView extends GetWidget<SingleEmailController> {
responsiveUtils: controller.responsiveUtils, responsiveUtils: controller.responsiveUtils,
attachments: controller.attachments, attachments: controller.attachments,
imagePaths: controller.imagePaths, imagePaths: controller.imagePaths,
onDragStarted: controller.mailboxDashBoardController.enableDraggableApp, onDragStarted: controller.mailboxDashBoardController.enableAttachmentDraggableApp,
onDragEnd: (details) { onDragEnd: (details) {
controller.mailboxDashBoardController.disableDraggableApp(); controller.mailboxDashBoardController.disableAttachmentDraggableApp();
}, },
downloadAttachmentAction: (attachment) => controller.handleDownloadAttachmentAction(context, attachment), downloadAttachmentAction: (attachment) => controller.handleDownloadAttachmentAction(context, attachment),
viewAttachmentAction: (attachment) => controller.handleViewAttachmentAction(context, attachment), viewAttachmentAction: (attachment) => controller.handleViewAttachmentAction(context, attachment),
@@ -376,7 +376,7 @@ class EmailView extends GetWidget<SingleEmailController> {
Obx(() => CalendarEventDetailWidget( Obx(() => CalendarEventDetailWidget(
calendarEvent: calendarEvent, calendarEvent: calendarEvent,
emailContent: controller.currentEmailLoaded?.htmlContent ?? '', emailContent: controller.currentEmailLoaded?.htmlContent ?? '',
isDraggableAppActive: controller.mailboxDashBoardController.isDraggableAppActive, isDraggableAppActive: controller.mailboxDashBoardController.isAttachmentDraggableAppActive,
onOpenComposerAction: controller.openNewComposerAction, onOpenComposerAction: controller.openNewComposerAction,
onOpenNewTabAction: controller.openNewTabAction, onOpenNewTabAction: controller.openNewTabAction,
onMailtoDelegateAction: controller.openMailToLink, onMailtoDelegateAction: controller.openMailToLink,
@@ -407,7 +407,14 @@ class EmailView extends GetWidget<SingleEmailController> {
mailtoDelegate: controller.openMailToLink, mailtoDelegate: controller.openMailToLink,
direction: AppUtils.getCurrentDirection(context), direction: AppUtils.getCurrentDirection(context),
), ),
if (controller.mailboxDashBoardController.isDraggableAppActive) if (controller.mailboxDashBoardController.isAttachmentDraggableAppActive)
PointerInterceptor(
child: SizedBox(
width: constraints.maxWidth,
height: constraints.maxHeight,
)
),
if (controller.mailboxDashBoardController.isLocalFileDraggableAppActive)
PointerInterceptor( PointerInterceptor(
child: SizedBox( child: SizedBox(
width: constraints.maxWidth, width: constraints.maxWidth,
@@ -1,38 +1,7 @@
import 'package:core/data/constants/constant.dart';
import 'package:core/domain/extensions/media_type_extension.dart';
import 'package:core/presentation/resources/image_paths.dart'; import 'package:core/presentation/resources/image_paths.dart';
import 'package:http_parser/http_parser.dart';
import 'package:model/email/attachment.dart'; import 'package:model/email/attachment.dart';
import 'package:tmail_ui_user/features/upload/domain/extensions/media_type_extension.dart';
extension AttachmentExtension on Attachment { extension AttachmentExtension on Attachment {
String getIcon(ImagePaths imagePaths) => type?.getIcon(imagePaths, fileName: name) ?? imagePaths.icFileEPup;
String getIcon(ImagePaths imagePaths, {MediaType? fileMediaType}) {
final mediaType = type ?? fileMediaType;
if (isDisplayedPDFIcon) {
return imagePaths.icFilePdf;
}
if (mediaType == null) {
return imagePaths.icFileEPup;
}
if (mediaType.isDocFile()) {
return imagePaths.icFileDocx;
} else if (mediaType.isExcelFile()) {
return imagePaths.icFileXlsx;
} else if (mediaType.isPowerPointFile()) {
return imagePaths.icFilePptx;
} else if (mediaType.isPdfFile()) {
return imagePaths.icFilePdf;
} else if (mediaType.isZipFile()) {
return imagePaths.icFileZip;
} else if (mediaType.isImageFile()) {
return imagePaths.icFilePng;
}
return imagePaths.icFileEPup;
}
bool get isDisplayedPDFIcon => type?.mimeType == Constant.pdfMimeType
|| (type?.mimeType == Constant.octetStreamMimeType
&& name?.endsWith(Constant.pdfExtension) == true);
} }
@@ -5,7 +5,6 @@ import 'dart:io';
import 'package:core/utils/app_logger.dart'; import 'package:core/utils/app_logger.dart';
import 'package:core/utils/platform_info.dart'; import 'package:core/utils/platform_info.dart';
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import 'package:get/get_connect/http/src/request/request.dart';
import 'package:jmap_dart_client/jmap/core/user_name.dart'; import 'package:jmap_dart_client/jmap/core/user_name.dart';
import 'package:model/account/authentication_type.dart'; import 'package:model/account/authentication_type.dart';
import 'package:model/account/password.dart'; import 'package:model/account/password.dart';
@@ -122,8 +121,6 @@ class AuthorizationInterceptors extends QueuedInterceptorsWrapper {
final uploadExtra = extraInRequest[FileUploader.uploadAttachmentExtraKey]; final uploadExtra = extraInRequest[FileUploader.uploadAttachmentExtraKey];
requestOptions.headers[HttpHeaders.authorizationHeader] = _getTokenAsBearerHeader(_token!.token); requestOptions.headers[HttpHeaders.authorizationHeader] = _getTokenAsBearerHeader(_token!.token);
requestOptions.headers[HttpHeaders.contentTypeHeader] = uploadExtra[FileUploader.typeExtraKey];
requestOptions.headers[HttpHeaders.contentLengthHeader] = uploadExtra[FileUploader.sizeExtraKey];
final newOptions = Options( final newOptions = Options(
method: requestOptions.method, method: requestOptions.method,
@@ -155,11 +152,16 @@ class AuthorizationInterceptors extends QueuedInterceptorsWrapper {
} }
Stream<List<int>>? _getDataUploadRequest(dynamic mapUploadExtra) { Stream<List<int>>? _getDataUploadRequest(dynamic mapUploadExtra) {
final currentPlatform = mapUploadExtra[FileUploader.platformExtraKey]; try {
if (currentPlatform == 'web') { String? filePath = mapUploadExtra[FileUploader.filePathExtraKey];
return BodyBytesStream.fromBytes(mapUploadExtra[FileUploader.bytesExtraKey]); if (filePath?.isNotEmpty == true) {
} else { return File(filePath!).openRead();
return File(mapUploadExtra[FileUploader.filePathExtraKey]).openRead(); } else {
return mapUploadExtra[FileUploader.streamDataExtraKey];
}
} catch(e) {
log('AuthorizationInterceptors::_getDataUploadRequest: Exception = $e');
return null;
} }
} }
@@ -216,8 +216,9 @@ class MailboxDashBoardController extends ReloadableController {
final searchMailboxActivated = RxBool(false); final searchMailboxActivated = RxBool(false);
final listSendingEmails = RxList<SendingEmail>(); final listSendingEmails = RxList<SendingEmail>();
final refreshingMailboxState = Rx<Either<Failure, Success>>(Right(UIState.idle)); final refreshingMailboxState = Rx<Either<Failure, Success>>(Right(UIState.idle));
final draggableAppState = Rxn<DraggableAppState>(); final attachmentDraggableAppState = Rxn<DraggableAppState>();
final isRecoveringDeletedMessage = RxBool(false); final isRecoveringDeletedMessage = RxBool(false);
final localFileDraggableAppState = Rxn<DraggableAppState>();
Session? sessionCurrent; Session? sessionCurrent;
Map<Role, MailboxId> mapDefaultMailboxIdByRole = {}; Map<Role, MailboxId> mapDefaultMailboxIdByRole = {};
@@ -2148,14 +2149,16 @@ class MailboxDashBoardController extends ReloadableController {
openMailboxAction(inboxPresentation); openMailboxAction(inboxPresentation);
} }
bool get isDraggableAppActive => draggableAppState.value == DraggableAppState.active; bool get isAttachmentDraggableAppActive => attachmentDraggableAppState.value == DraggableAppState.active;
void enableDraggableApp() { bool get isLocalFileDraggableAppActive => localFileDraggableAppState.value == DraggableAppState.active;
draggableAppState.value = DraggableAppState.active;
void enableAttachmentDraggableApp() {
attachmentDraggableAppState.value = DraggableAppState.active;
} }
void disableDraggableApp() { void disableAttachmentDraggableApp() {
draggableAppState.value = DraggableAppState.inActive; attachmentDraggableAppState.value = DraggableAppState.inActive;
} }
void saveEmailToDraft({required SaveToDraftArguments arguments}) { void saveEmailToDraft({required SaveToDraftArguments arguments}) {
@@ -27,10 +27,7 @@ import 'package:worker_manager/worker_manager.dart' as worker;
class FileUploader { class FileUploader {
static const String uploadAttachmentExtraKey = 'upload-attachment'; static const String uploadAttachmentExtraKey = 'upload-attachment';
static const String platformExtraKey = 'platform'; static const String streamDataExtraKey = 'streamData';
static const String bytesExtraKey = 'bytes';
static const String typeExtraKey = 'type';
static const String sizeExtraKey = 'size';
static const String filePathExtraKey = 'path'; static const String filePathExtraKey = 'path';
final DioClient _dioClient; final DioClient _dioClient;
@@ -94,10 +91,7 @@ class FileUploader {
final mapExtra = <String, dynamic>{ final mapExtra = <String, dynamic>{
uploadAttachmentExtraKey: { uploadAttachmentExtraKey: {
platformExtraKey: 'mobile',
filePathExtraKey: argsUpload.mobileFileUpload.filePath, filePathExtraKey: argsUpload.mobileFileUpload.filePath,
typeExtraKey: argsUpload.mobileFileUpload.mimeType,
sizeExtraKey: argsUpload.mobileFileUpload.fileSize,
} }
}; };
@@ -109,7 +103,7 @@ class FileUploader {
), ),
data: File(argsUpload.mobileFileUpload.filePath).openRead(), data: File(argsUpload.mobileFileUpload.filePath).openRead(),
onSendProgress: (count, total) { onSendProgress: (count, total) {
log('FileUploader::_handleUploadAttachmentAction():onSendProgress: [${argsUpload.uploadId.id}] = $count'); log('FileUploader::_handleUploadAttachmentAction():onSendProgress: FILE[${argsUpload.uploadId.id}] : { PROGRESS = $count | TOTAL = $total}');
sendPort.send( sendPort.send(
UploadingAttachmentUploadState( UploadingAttachmentUploadState(
argsUpload.uploadId, argsUpload.uploadId,
@@ -139,10 +133,7 @@ class FileUploader {
final mapExtra = <String, dynamic>{ final mapExtra = <String, dynamic>{
uploadAttachmentExtraKey: { uploadAttachmentExtraKey: {
platformExtraKey: 'web', streamDataExtraKey: BodyBytesStream.fromBytes(fileInfo.bytes!),
bytesExtraKey: fileInfo.bytes,
typeExtraKey: fileInfo.mimeType,
sizeExtraKey: fileInfo.fileSize,
} }
}; };
@@ -155,7 +146,7 @@ class FileUploader {
data: BodyBytesStream.fromBytes(fileInfo.bytes!), data: BodyBytesStream.fromBytes(fileInfo.bytes!),
cancelToken: cancelToken, cancelToken: cancelToken,
onSendProgress: (count, total) { onSendProgress: (count, total) {
log('FileUploader::_handleUploadAttachmentActionOnWeb():onSendProgress: [${uploadId.id}] = $count'); log('FileUploader::_handleUploadAttachmentActionOnWeb():onSendProgress: FILE[${uploadId.id}] : { PROGRESS = $count | TOTAL = $total}');
onSendController.add( onSendController.add(
Right(UploadingAttachmentUploadState( Right(UploadingAttachmentUploadState(
uploadId, uploadId,
@@ -0,0 +1 @@
class PickFileCanceledException implements Exception {}
@@ -3,5 +3,15 @@ import 'package:model/upload/file_info.dart';
import 'package:tmail_ui_user/features/upload/domain/model/mobile_file_upload.dart'; import 'package:tmail_ui_user/features/upload/domain/model/mobile_file_upload.dart';
extension FileInfoExtension on FileInfo { extension FileInfoExtension on FileInfo {
MobileFileUpload toMobileFileUpload() => MobileFileUpload(fileName, filePath, fileSize, mimeType); MobileFileUpload toMobileFileUpload() => MobileFileUpload(fileName, filePath!, fileSize, mimeType);
FileInfo withInline() => FileInfo(
fileName: fileName,
fileSize: fileSize,
filePath: filePath,
bytes: bytes,
readStream: readStream,
type: type,
isInline: true,
isShared: isShared);
} }
@@ -3,5 +3,5 @@ import 'package:model/upload/file_info.dart';
extension ListFileInfoExtension on List<FileInfo> { extension ListFileInfoExtension on List<FileInfo> {
List<int> get listSize => map((file) => file.fileSize).toList(); List<int> get listSize => map((file) => file.fileSize).toList();
num get totalSize => listSize.reduce((sum, size) => sum + size); num get totalSize => listSize.isEmpty ? 0 : listSize.reduce((sum, size) => sum + size);
} }
@@ -0,0 +1,28 @@
import 'package:core/domain/extensions/media_type_extension.dart';
import 'package:core/presentation/resources/image_paths.dart';
import 'package:http_parser/http_parser.dart';
extension MediaTypeExtension on MediaType {
String getIcon(ImagePaths imagePaths, {String? fileName}) {
if (validatePDFIcon(fileName: fileName) == true) {
return imagePaths.icFilePdf;
} else if (isDocFile()) {
return imagePaths.icFileDocx;
} else if (isExcelFile()) {
return imagePaths.icFileXlsx;
} else if (isPowerPointFile()) {
return imagePaths.icFilePptx;
} else if (isPdfFile()) {
return imagePaths.icFilePdf;
} else if (isZipFile()) {
return imagePaths.icFileZip;
} else if (isImageFile()) {
return imagePaths.icFilePng;
} else {
return imagePaths.icFileEPup;
}
}
bool validatePDFIcon({required String? fileName}) => mimeType == 'application/pdf' ||
(mimeType == 'application/octet-stream' && fileName?.endsWith('.pdf') == true);
}
@@ -0,0 +1,13 @@
import 'package:core/utils/platform_info.dart';
import 'package:file_picker/file_picker.dart';
import 'package:model/upload/file_info.dart';
extension PlatformFileExtension on PlatformFile {
FileInfo toFileInfo() => FileInfo(
fileName: name,
fileSize: size,
filePath: PlatformInfo.isWeb ? '' : path ?? '',
bytes: bytes,
readStream: readStream
);
}
@@ -31,15 +31,11 @@ class SuccessAttachmentUploadState extends Success {
final UploadTaskId uploadId; final UploadTaskId uploadId;
final Attachment attachment; final Attachment attachment;
final FileInfo fileInfo; final FileInfo fileInfo;
final bool fromFileShared;
SuccessAttachmentUploadState( SuccessAttachmentUploadState(
this.uploadId, this.uploadId,
this.attachment, this.attachment,
this.fileInfo, this.fileInfo
{
this.fromFileShared = false
}
); );
@override @override
@@ -47,7 +43,6 @@ class SuccessAttachmentUploadState extends Success {
uploadId, uploadId,
attachment, attachment,
fileInfo, fileInfo,
fromFileShared,
]; ];
} }
@@ -2,6 +2,8 @@ import 'package:core/presentation/state/failure.dart';
import 'package:core/presentation/state/success.dart'; import 'package:core/presentation/state/success.dart';
import 'package:model/upload/file_info.dart'; import 'package:model/upload/file_info.dart';
class LocalFilePickerLoading extends LoadingState {}
class LocalFilePickerSuccess extends UIState { class LocalFilePickerSuccess extends UIState {
final List<FileInfo> pickedFiles; final List<FileInfo> pickedFiles;
@@ -14,6 +16,4 @@ class LocalFilePickerSuccess extends UIState {
class LocalFilePickerFailure extends FeatureFailure { class LocalFilePickerFailure extends FeatureFailure {
LocalFilePickerFailure(dynamic exception) : super(exception: exception); LocalFilePickerFailure(dynamic exception) : super(exception: exception);
} }
class LocalFilePickerCancel extends FeatureFailure {}
@@ -0,0 +1,19 @@
import 'package:core/presentation/state/failure.dart';
import 'package:core/presentation/state/success.dart';
import 'package:model/upload/file_info.dart';
class LocalImagePickerLoading extends LoadingState {}
class LocalImagePickerSuccess extends UIState {
final FileInfo fileInfo;
LocalImagePickerSuccess(this.fileInfo);
@override
List<Object> get props => [fileInfo];
}
class LocalImagePickerFailure extends FeatureFailure {
LocalImagePickerFailure(dynamic exception) : super(exception: exception);
}
@@ -4,7 +4,8 @@ import 'package:core/presentation/state/success.dart';
import 'package:core/utils/platform_info.dart'; import 'package:core/utils/platform_info.dart';
import 'package:dartz/dartz.dart'; import 'package:dartz/dartz.dart';
import 'package:file_picker/file_picker.dart'; import 'package:file_picker/file_picker.dart';
import 'package:model/upload/file_info.dart'; import 'package:tmail_ui_user/features/upload/domain/exceptions/pick_file_exception.dart';
import 'package:tmail_ui_user/features/upload/domain/extensions/platform_file_extension.dart';
import 'package:tmail_ui_user/features/upload/domain/state/local_file_picker_state.dart'; import 'package:tmail_ui_user/features/upload/domain/state/local_file_picker_state.dart';
class LocalFilePickerInteractor { class LocalFilePickerInteractor {
@@ -13,23 +14,22 @@ class LocalFilePickerInteractor {
Stream<Either<Failure, Success>> execute({FileType fileType = FileType.any}) async* { Stream<Either<Failure, Success>> execute({FileType fileType = FileType.any}) async* {
try { try {
yield Right<Failure, Success>(LocalFilePickerLoading());
final filesResult = await FilePicker.platform.pickFiles( final filesResult = await FilePicker.platform.pickFiles(
type: fileType, type: fileType,
allowMultiple: true, allowMultiple: true,
withData: PlatformInfo.isWeb withData: PlatformInfo.isWeb,
withReadStream: PlatformInfo.isMobile
); );
if (filesResult != null && filesResult.files.isNotEmpty) {
final fileInfoResults = filesResult.files if (filesResult?.files.isNotEmpty == true) {
.map((platformFile) => FileInfo( final listFileInfo = filesResult!.files
platformFile.name, .map((platformFile) => platformFile.toFileInfo())
PlatformInfo.isWeb ? '' : platformFile.path ?? '', .toList();
platformFile.size, yield Right<Failure, Success>(LocalFilePickerSuccess(listFileInfo));
bytes: PlatformInfo.isWeb ? platformFile.bytes : null
))
.toList();
yield Right<Failure, Success>(LocalFilePickerSuccess(fileInfoResults));
} else { } else {
yield Left<Failure, Success>(LocalFilePickerCancel()); yield Left<Failure, Success>(LocalFilePickerFailure(PickFileCanceledException()));
} }
} catch (exception) { } catch (exception) {
yield Left<Failure, Success>(LocalFilePickerFailure(exception)); yield Left<Failure, Success>(LocalFilePickerFailure(exception));
@@ -0,0 +1,35 @@
import 'package:core/presentation/state/failure.dart';
import 'package:core/presentation/state/success.dart';
import 'package:core/utils/platform_info.dart';
import 'package:dartz/dartz.dart';
import 'package:file_picker/file_picker.dart';
import 'package:tmail_ui_user/features/upload/domain/exceptions/pick_file_exception.dart';
import 'package:tmail_ui_user/features/upload/domain/extensions/platform_file_extension.dart';
import 'package:tmail_ui_user/features/upload/domain/state/local_image_picker_state.dart';
class LocalImagePickerInteractor {
LocalImagePickerInteractor();
Stream<Either<Failure, Success>> execute() async* {
try {
yield Right<Failure, Success>(LocalImagePickerLoading());
final filePickerResult = await FilePicker.platform.pickFiles(
type: FileType.image,
allowMultiple: false,
withReadStream: PlatformInfo.isMobile,
withData: PlatformInfo.isWeb
);
if (filePickerResult?.files.isNotEmpty == true) {
final fileInfo = filePickerResult!.files.first.toFileInfo();
yield Right<Failure, Success>(LocalImagePickerSuccess(fileInfo));
} else {
yield Left<Failure, Success>(LocalImagePickerFailure(PickFileCanceledException()));
}
} catch (exception) {
yield Left<Failure, Success>(LocalImagePickerFailure(exception));
}
}
}
@@ -14,13 +14,13 @@ import 'package:get/get.dart';
import 'package:jmap_dart_client/jmap/mail/email/email_body_part.dart'; import 'package:jmap_dart_client/jmap/mail/email/email_body_part.dart';
import 'package:model/email/attachment.dart'; import 'package:model/email/attachment.dart';
import 'package:model/extensions/attachment_extension.dart'; import 'package:model/extensions/attachment_extension.dart';
import 'package:model/extensions/list_attachment_extension.dart';
import 'package:model/upload/file_info.dart'; import 'package:model/upload/file_info.dart';
import 'package:tmail_ui_user/features/base/base_controller.dart'; import 'package:tmail_ui_user/features/base/base_controller.dart';
import 'package:tmail_ui_user/features/base/state/base_ui_state.dart'; import 'package:tmail_ui_user/features/base/state/base_ui_state.dart';
import 'package:tmail_ui_user/features/composer/domain/state/upload_attachment_state.dart'; import 'package:tmail_ui_user/features/composer/domain/state/upload_attachment_state.dart';
import 'package:tmail_ui_user/features/composer/domain/usecases/upload_attachment_interactor.dart'; import 'package:tmail_ui_user/features/composer/domain/usecases/upload_attachment_interactor.dart';
import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart';
import 'package:tmail_ui_user/features/upload/domain/extensions/list_file_info_extension.dart';
import 'package:tmail_ui_user/features/upload/domain/model/upload_task_id.dart'; import 'package:tmail_ui_user/features/upload/domain/model/upload_task_id.dart';
import 'package:tmail_ui_user/features/upload/domain/state/attachment_upload_state.dart'; import 'package:tmail_ui_user/features/upload/domain/state/attachment_upload_state.dart';
import 'package:tmail_ui_user/features/upload/presentation/extensions/upload_attachment_extension.dart'; import 'package:tmail_ui_user/features/upload/presentation/extensions/upload_attachment_extension.dart';
@@ -80,7 +80,7 @@ class UploadController extends BaseController {
failure.uploadId, failure.uploadId,
(currentState) => currentState?.copyWith(uploadStatus: UploadFileStatus.uploadFailed)); (currentState) => currentState?.copyWith(uploadStatus: UploadFileStatus.uploadFailed));
deleteFileUploaded(failure.uploadId); deleteFileUploaded(failure.uploadId);
_handleUploadAttachmentsFailure(failure); _showToastMessageWhenUploadAttachmentsFailure(failure);
} }
}, },
(success) { (success) {
@@ -115,7 +115,7 @@ class UploadController extends BaseController {
); );
_refreshListUploadAttachmentState(); _refreshListUploadAttachmentState();
_handleUploadAttachmentsSuccess(success); _showToastMessageWhenUploadAttachmentsSuccess(success);
} }
} }
); );
@@ -162,7 +162,7 @@ class UploadController extends BaseController {
); );
final uploadFileState = _uploadingStateInlineFiles.getUploadFileStateById(success.uploadId); final uploadFileState = _uploadingStateInlineFiles.getUploadFileStateById(success.uploadId);
log('UploadController::_handleProgressUploadInlineImageStateStream:uploadId: ${uploadFileState?.uploadTaskId} | fromFileShared: ${uploadFileState?.fromFileShared}'); log('UploadController::_handleProgressUploadInlineImageStateStream:uploadId: ${uploadFileState?.uploadTaskId} | fromFileShared: ${uploadFileState?.file?.isShared}');
_uploadingStateInlineFiles.updateElementByUploadTaskId( _uploadingStateInlineFiles.updateElementByUploadTaskId(
success.uploadId, success.uploadId,
@@ -179,7 +179,6 @@ class UploadController extends BaseController {
success.uploadId, success.uploadId,
inlineAttachment, inlineAttachment,
success.fileInfo, success.fileInfo,
fromFileShared: uploadFileState?.fromFileShared ?? false
); );
_handleUploadInlineAttachmentsSuccess(newUploadSuccess); _handleUploadInlineAttachmentsSuccess(newUploadSuccess);
} }
@@ -203,27 +202,24 @@ class UploadController extends BaseController {
_refreshListUploadAttachmentState(); _refreshListUploadAttachmentState();
} }
Future<void> justUploadAttachmentsAction(List<FileInfo> uploadFiles, Uri uploadUri) { Future<void> justUploadAttachmentsAction({
required List<FileInfo> uploadFiles,
required Uri uploadUri,
}) {
return Future.forEach<FileInfo>(uploadFiles, (uploadFile) async { return Future.forEach<FileInfo>(uploadFiles, (uploadFile) async {
await uploadFileAction(uploadFile, uploadUri); await uploadFileAction(uploadFile: uploadFile, uploadUri: uploadUri);
}); });
} }
Future<void> uploadFileAction( Future<void> uploadFileAction({
FileInfo uploadFile, required FileInfo uploadFile,
Uri uploadUri, required Uri uploadUri,
{ }) {
bool isInline = false, log('UploadController::_uploadFile():fileName: ${uploadFile.fileName} | mimeType: ${uploadFile.mimeType} | isInline: ${uploadFile.isInline} | fromFileShared: ${uploadFile.isShared}');
bool fromFileShared = false,
}
) {
log('UploadController::_uploadFile():fileName: ${uploadFile.fileName} | isInline: $isInline | fromFileShared: $fromFileShared');
consumeState(_uploadAttachmentInteractor.execute( consumeState(_uploadAttachmentInteractor.execute(
uploadFile, uploadFile,
uploadUri, uploadUri,
cancelToken: CancelToken(), cancelToken: CancelToken(),
isInline: isInline,
fromFileShared: fromFileShared
)); ));
return Future.value(); return Future.value();
} }
@@ -244,6 +240,16 @@ class UploadController extends BaseController {
.toList(); .toList();
} }
List<FileInfo> get attachmentsPicked {
if (listUploadAttachments.isEmpty) {
return List.empty();
}
return listUploadAttachments
.map((fileState) => fileState.file)
.whereNotNull()
.toList();
}
Set<EmailBodyPart>? generateAttachments() { Set<EmailBodyPart>? generateAttachments() {
if (attachmentsUploaded.isEmpty) { if (attachmentsUploaded.isEmpty) {
return null; return null;
@@ -254,7 +260,7 @@ class UploadController extends BaseController {
.toSet(); .toSet();
} }
void _handleUploadAttachmentsFailure(ErrorAttachmentUploadState failure) { void _showToastMessageWhenUploadAttachmentsFailure(ErrorAttachmentUploadState failure) {
if (currentContext != null && currentOverlayContext != null) { if (currentContext != null && currentOverlayContext != null) {
appToast.showToastErrorMessage( appToast.showToastErrorMessage(
currentOverlayContext!, currentOverlayContext!,
@@ -264,7 +270,7 @@ class UploadController extends BaseController {
} }
} }
void _handleUploadAttachmentsSuccess(SuccessAttachmentUploadState success) { void _showToastMessageWhenUploadAttachmentsSuccess(SuccessAttachmentUploadState success) {
if (currentContext != null && currentOverlayContext != null && _uploadingStateFiles.allSuccess) { if (currentContext != null && currentOverlayContext != null && _uploadingStateFiles.allSuccess) {
appToast.showToastSuccessMessage( appToast.showToastSuccessMessage(
currentOverlayContext!, currentOverlayContext!,
@@ -275,7 +281,7 @@ class UploadController extends BaseController {
} }
bool isExceededMaxSizeAttachmentsPerEmail({num totalSizePreparedFiles = 0}) { bool isExceededMaxSizeAttachmentsPerEmail({num totalSizePreparedFiles = 0}) {
final currentTotalSize = attachmentsUploaded.totalSize + inlineAttachmentsUploaded.totalSize + totalSizePreparedFiles; final currentTotalSize = attachmentsPicked.totalSize + inlineAttachmentsPicked.totalSize + totalSizePreparedFiles;
final maxSizeAttachmentsPerEmail = _mailboxDashBoardController.maxSizeAttachmentsPerEmail?.value; final maxSizeAttachmentsPerEmail = _mailboxDashBoardController.maxSizeAttachmentsPerEmail?.value;
log('UploadController::isExceededMaxSizeAttachmentsPerEmail(): currentTotalSize = $currentTotalSize | maxSizeAttachmentsPerEmail = $maxSizeAttachmentsPerEmail'); log('UploadController::isExceededMaxSizeAttachmentsPerEmail(): currentTotalSize = $currentTotalSize | maxSizeAttachmentsPerEmail = $maxSizeAttachmentsPerEmail');
if (maxSizeAttachmentsPerEmail != null) { if (maxSizeAttachmentsPerEmail != null) {
@@ -285,16 +291,17 @@ class UploadController extends BaseController {
} }
} }
bool isExceededMaxSizeFilesAttachedInComposer({num totalSizePreparedFiles = 0}) { bool isExceededWarningAttachmentFileSizeInComposer({num totalSizePreparedFiles = 0}) {
final currentTotalSizeAttachments = attachmentsUploaded.totalSize + totalSizePreparedFiles; final currentTotalSizeAttachments = attachmentsPicked.totalSize + totalSizePreparedFiles;
const maximumBytesSizeFileAttachedInComposer = AppConfig.maximumMegabytesSizeFileAttachedInComposer * 1024 * 1024; const maximumBytesSizeFileAttachedInComposer = AppConfig.warningAttachmentFileSizeInMegabytes * 1024 * 1024;
log('UploadController::isExceededMaxSizeFilesAttachedInComposer(): currentTotalSizeAttachments = $currentTotalSizeAttachments | maximumBytesSizeFileAttachedInComposer = $maximumBytesSizeFileAttachedInComposer'); log('UploadController::isExceededMaxSizeFilesAttachedInComposer(): currentTotalSizeAttachments = $currentTotalSizeAttachments | maximumBytesSizeFileAttachedInComposer = $maximumBytesSizeFileAttachedInComposer');
return currentTotalSizeAttachments > maximumBytesSizeFileAttachedInComposer; return currentTotalSizeAttachments > maximumBytesSizeFileAttachedInComposer;
} }
void validateTotalSizeAttachmentsBeforeUpload({ void validateTotalSizeAttachmentsBeforeUpload({
required num totalSizePreparedFiles, required num totalSizePreparedFiles,
VoidCallback? callbackAction num? totalSizePreparedFilesWithDispositionAttachment,
VoidCallback? onValidationSuccess
}) { }) {
log('UploadController::_validateTotalSizeAttachmentsBeforeUpload: totalSizePreparedFiles = $totalSizePreparedFiles'); log('UploadController::_validateTotalSizeAttachmentsBeforeUpload: totalSizePreparedFiles = $totalSizePreparedFiles');
if (isExceededMaxSizeAttachmentsPerEmail(totalSizePreparedFiles: totalSizePreparedFiles)) { if (isExceededMaxSizeAttachmentsPerEmail(totalSizePreparedFiles: totalSizePreparedFiles)) {
@@ -307,7 +314,7 @@ class UploadController extends BaseController {
return; return;
} }
if (isExceededMaxSizeFilesAttachedInComposer(totalSizePreparedFiles: totalSizePreparedFiles)) { if (isExceededWarningAttachmentFileSizeInComposer(totalSizePreparedFiles: totalSizePreparedFilesWithDispositionAttachment ?? totalSizePreparedFiles)) {
if (currentContext == null) { if (currentContext == null) {
log('UploadController::_validateTotalSizeAttachmentsBeforeUpload: CONTEXT IS NULL'); log('UploadController::_validateTotalSizeAttachmentsBeforeUpload: CONTEXT IS NULL');
return; return;
@@ -315,12 +322,34 @@ class UploadController extends BaseController {
_showWarningDialogWhenExceededMaxSizeFilesAttachedInComposer( _showWarningDialogWhenExceededMaxSizeFilesAttachedInComposer(
context: currentContext!, context: currentContext!,
confirmAction: callbackAction confirmAction: () async {
await Future.delayed(
const Duration(milliseconds: 100),
onValidationSuccess
);
}
); );
return; return;
} }
callbackAction?.call(); onValidationSuccess?.call();
}
void validateTotalSizeInlineAttachmentsBeforeUpload({
required num totalSizePreparedFiles,
VoidCallback? onValidationSuccess
}) {
if (isExceededMaxSizeAttachmentsPerEmail(totalSizePreparedFiles: totalSizePreparedFiles)) {
if (currentContext == null) {
log('UploadController::validateTotalSizeInlineAttachmentsBeforeUpload: CONTEXT IS NULL');
return;
}
_showConfirmDialogWhenExceededMaxSizeAttachmentsPerEmail(context: currentContext!);
return;
}
onValidationSuccess?.call();
} }
void _showConfirmDialogWhenExceededMaxSizeAttachmentsPerEmail({required BuildContext context}) { void _showConfirmDialogWhenExceededMaxSizeAttachmentsPerEmail({required BuildContext context}) {
@@ -340,7 +369,7 @@ class UploadController extends BaseController {
showConfirmDialogAction( showConfirmDialogAction(
context, context,
title: '', title: '',
AppLocalizations.of(context).messageWarningDialogWhenExceedMaximumFileSizeComposer, AppLocalizations.of(context).warningMessageWhenExceedGenerallySizeInComposer,
AppLocalizations.of(context).continueAction, AppLocalizations.of(context).continueAction,
cancelTitle: AppLocalizations.of(context).cancel, cancelTitle: AppLocalizations.of(context).cancel,
alignCenter: true, alignCenter: true,
@@ -394,6 +423,17 @@ class UploadController extends BaseController {
.toList(); .toList();
} }
List<FileInfo> get inlineAttachmentsPicked {
if (_uploadingStateInlineFiles.uploadingStateFiles.isEmpty) {
return List.empty();
}
return _uploadingStateInlineFiles.uploadingStateFiles
.whereNotNull()
.map((fileState) => fileState.file)
.whereNotNull()
.toList();
}
Map<String, Attachment> get mapInlineAttachments { Map<String, Attachment> get mapInlineAttachments {
final inlineAttachments = _uploadingStateInlineFiles.uploadingStateFiles final inlineAttachments = _uploadingStateInlineFiles.uploadingStateFiles
.whereNotNull() .whereNotNull()
@@ -415,42 +455,47 @@ class UploadController extends BaseController {
return mapInlineAttachments; return mapInlineAttachments;
} }
void _handleUploadAttachmentFailure(UploadAttachmentFailure failure) {
if (currentContext != null && currentOverlayContext != null) {
appToast.showToastErrorMessage(
currentOverlayContext!,
failure.fileInfo.isInline == true
? AppLocalizations.of(currentContext!).thisImageCannotBeAdded
: AppLocalizations.of(currentContext!).can_not_upload_this_file_as_attachments,
leadingSVGIconColor: Colors.white,
leadingSVGIcon: failure.fileInfo.isInline == true
? imagePaths.icInsertImage
: imagePaths.icAttachment
);
}
}
void _handleUploadAttachmentSuccess(UploadAttachmentSuccess success) async {
if (success.uploadAttachment.fileInfo.isInline == true) {
_uploadingStateInlineFiles.add(success.uploadAttachment.toUploadFileState());
await _progressUploadInlineImageStateStreamGroup.add(success.uploadAttachment.progressState);
} else {
_uploadingStateFiles.add(success.uploadAttachment.toUploadFileState());
await _progressUploadStateStreamGroup.add(success.uploadAttachment.progressState);
_refreshListUploadAttachmentState();
}
}
@override @override
void handleFailureViewState(Failure failure) async { void handleFailureViewState(Failure failure) {
super.handleFailureViewState(failure);
if (failure is UploadAttachmentFailure) { if (failure is UploadAttachmentFailure) {
if (failure.isInline) { _handleUploadAttachmentFailure(failure);
if (currentContext != null && currentOverlayContext != null) { } else {
appToast.showToastErrorMessage( super.handleFailureViewState(failure);
currentOverlayContext!,
AppLocalizations.of(currentContext!).thisImageCannotBeAdded,
leadingSVGIconColor: Colors.white,
leadingSVGIcon: imagePaths.icInsertImage);
}
} else {
if (currentContext != null && currentOverlayContext != null) {
appToast.showToastErrorMessage(
currentOverlayContext!,
AppLocalizations.of(currentContext!).can_not_upload_this_file_as_attachments,
leadingSVGIconColor: Colors.white,
leadingSVGIcon: imagePaths.icAttachment);
}
}
} }
} }
@override @override
void handleSuccessViewState(Success success) async { void handleSuccessViewState(Success success) async {
super.handleSuccessViewState(success);
if (success is UploadAttachmentSuccess) { if (success is UploadAttachmentSuccess) {
if (success.isInline) { _handleUploadAttachmentSuccess(success);
_uploadingStateInlineFiles.add(success.uploadAttachment.toUploadFileState(fromFileShared: success.fromFileShared)); } else {
await _progressUploadInlineImageStateStreamGroup.add(success.uploadAttachment.progressState); super.handleSuccessViewState(success);
} else {
_uploadingStateFiles.add(success.uploadAttachment.toUploadFileState());
await _progressUploadStateStreamGroup.add(success.uploadAttachment.progressState);
_refreshListUploadAttachmentState();
}
} }
} }
} }
@@ -4,12 +4,11 @@ import 'package:tmail_ui_user/features/upload/presentation/model/upload_file_sta
extension UploadAttachmentExtension on UploadAttachment { extension UploadAttachmentExtension on UploadAttachment {
UploadFileState toUploadFileState({bool fromFileShared = false}) { UploadFileState toUploadFileState() {
return UploadFileState( return UploadFileState(
uploadTaskId, uploadTaskId,
file: fileInfo, file: fileInfo,
cancelToken: cancelToken, cancelToken: cancelToken,
fromFileShared: fromFileShared,
); );
} }
} }
@@ -1,11 +1,12 @@
import 'package:core/presentation/resources/image_paths.dart'; import 'package:core/presentation/resources/image_paths.dart';
import 'package:core/utils/app_logger.dart';
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import 'package:equatable/equatable.dart'; import 'package:equatable/equatable.dart';
import 'package:http_parser/http_parser.dart'; import 'package:http_parser/http_parser.dart';
import 'package:model/email/attachment.dart'; import 'package:model/email/attachment.dart';
import 'package:model/upload/file_info.dart'; import 'package:model/upload/file_info.dart';
import 'package:tmail_ui_user/features/email/presentation/extensions/attachment_extension.dart'; import 'package:tmail_ui_user/features/upload/domain/extensions/media_type_extension.dart';
import 'package:tmail_ui_user/features/upload/domain/model/upload_task_id.dart'; import 'package:tmail_ui_user/features/upload/domain/model/upload_task_id.dart';
import 'package:tmail_ui_user/features/upload/presentation/model/upload_file_status.dart'; import 'package:tmail_ui_user/features/upload/presentation/model/upload_file_status.dart';
@@ -17,7 +18,6 @@ class UploadFileState with EquatableMixin {
final int uploadingProgress; final int uploadingProgress;
final Attachment? attachment; final Attachment? attachment;
final CancelToken? cancelToken; final CancelToken? cancelToken;
final bool fromFileShared;
UploadFileState( UploadFileState(
this.uploadTaskId, this.uploadTaskId,
@@ -27,7 +27,6 @@ class UploadFileState with EquatableMixin {
this.uploadingProgress = 0, this.uploadingProgress = 0,
this.attachment, this.attachment,
this.cancelToken, this.cancelToken,
this.fromFileShared = false,
} }
); );
@@ -38,7 +37,6 @@ class UploadFileState with EquatableMixin {
int? uploadingProgress, int? uploadingProgress,
Attachment? attachment, Attachment? attachment,
CancelToken? cancelToken, CancelToken? cancelToken,
bool? fromFileShared,
}) { }) {
return UploadFileState( return UploadFileState(
uploadTaskId ?? this.uploadTaskId, uploadTaskId ?? this.uploadTaskId,
@@ -47,7 +45,6 @@ class UploadFileState with EquatableMixin {
uploadingProgress: uploadingProgress ?? this.uploadingProgress, uploadingProgress: uploadingProgress ?? this.uploadingProgress,
attachment: attachment ?? this.attachment, attachment: attachment ?? this.attachment,
cancelToken: cancelToken ?? this.cancelToken, cancelToken: cancelToken ?? this.cancelToken,
fromFileShared: fromFileShared ?? this.fromFileShared
); );
} }
@@ -72,11 +69,16 @@ class UploadFileState with EquatableMixin {
double get percentUploading => uploadingProgress / 100; double get percentUploading => uploadingProgress / 100;
String getIcon(ImagePaths imagePaths) { String getIcon(ImagePaths imagePaths) {
var mediaType = attachment?.type; try {
if (mediaType == null && file != null) { MediaType? mediaType = attachment?.type;
mediaType = MediaType.parse(file!.mimeType); if (mediaType == null && file != null) {
mediaType = MediaType.parse(file!.mimeType);
}
return mediaType?.getIcon(imagePaths, fileName: fileName) ?? imagePaths.icFileEPup;
} catch (e) {
logError('UploadFileState::getIcon: Exception: $e');
return imagePaths.icFileEPup;
} }
return attachment?.getIcon(imagePaths, fileMediaType: mediaType) ?? imagePaths.icFileEPup;
} }
@override @override
@@ -87,6 +89,5 @@ class UploadFileState with EquatableMixin {
uploadingProgress, uploadingProgress,
attachment, attachment,
cancelToken, cancelToken,
fromFileShared,
]; ];
} }
+2 -2
View File
@@ -3239,8 +3239,8 @@
"placeholders_order": [], "placeholders_order": [],
"placeholders": {} "placeholders": {}
}, },
"messageWarningDialogWhenExceedMaximumFileSizeComposer": "Your message is larger than the size generally accepted by third party email systems. If you confirm sending this mail, there is a risk that it gets rejected by your recipient system.", "warningMessageWhenExceedGenerallySizeInComposer": "Your message is larger than the size generally accepted by third party email systems. If you confirm sending this mail, there is a risk that it gets rejected by your recipient system.",
"@messageWarningDialogWhenExceedMaximumFileSizeComposer": { "@warningMessageWhenExceedGenerallySizeInComposer": {
"type": "text", "type": "text",
"placeholders_order": [], "placeholders_order": [],
"placeholders": {} "placeholders": {}
+2 -2
View File
@@ -3936,8 +3936,8 @@
"placeholders_order": [], "placeholders_order": [],
"placeholders": {} "placeholders": {}
}, },
"messageWarningDialogWhenExceedMaximumFileSizeComposer": "Votre message dépasse la taille généralement acceptée pour un email. \nSi vous confirmez l'envoi il y a un risque que le mail soit rejeté par le système de votre interlocuteur.", "warningMessageWhenExceedGenerallySizeInComposer": "Votre message dépasse la taille généralement acceptée pour un email. \nSi vous confirmez l'envoi il y a un risque que le mail soit rejeté par le système de votre interlocuteur.",
"@messageWarningDialogWhenExceedMaximumFileSizeComposer": { "@warningMessageWhenExceedGenerallySizeInComposer": {
"type": "text", "type": "text",
"placeholders_order": [], "placeholders_order": [],
"placeholders": {} "placeholders": {}
+15 -3
View File
@@ -1,5 +1,5 @@
{ {
"@@last_modified": "2024-02-29T16:08:18.479839", "@@last_modified": "2024-03-15T21:34:52.177175",
"initializing_data": "Initializing data...", "initializing_data": "Initializing data...",
"@initializing_data": { "@initializing_data": {
"type": "text", "type": "text",
@@ -3750,8 +3750,8 @@
"placeholders_order": [], "placeholders_order": [],
"placeholders": {} "placeholders": {}
}, },
"messageWarningDialogWhenExceedMaximumFileSizeComposer": "Your message is larger than the size generally accepted by third party email systems. If you confirm sending this mail, there is a risk that it gets rejected by your recipient system.", "warningMessageWhenExceedGenerallySizeInComposer": "Your message is larger than the size generally accepted by third party email systems. If you confirm sending this mail, there is a risk that it gets rejected by your recipient system.",
"@messageWarningDialogWhenExceedMaximumFileSizeComposer": { "@warningMessageWhenExceedGenerallySizeInComposer": {
"type": "text", "type": "text",
"placeholders_order": [], "placeholders_order": [],
"placeholders": {} "placeholders": {}
@@ -3761,5 +3761,17 @@
"type": "text", "type": "text",
"placeholders_order": [], "placeholders_order": [],
"placeholders": {} "placeholders": {}
},
"thisFileCannotBePicked": "This file cannot be picked.",
"@thisFileCannotBePicked": {
"type": "text",
"placeholders_order": [],
"placeholders": {}
},
"loadingPleaseWait": "Loading... Please wait!",
"@loadingPleaseWait": {
"type": "text",
"placeholders_order": [],
"placeholders": {}
} }
} }
+2
View File
@@ -3944,6 +3944,8 @@
}, },
"messageWarningDialogWhenExceedMaximumFileSizeComposer": "Thư của bạn lớn hơn kích thước thường được hệ thống email của bên thứ ba chấp nhận. Nếu bạn xác nhận đã gửi thư này, có nguy cơ nó sẽ bị hệ thống người nhận từ chối.", "messageWarningDialogWhenExceedMaximumFileSizeComposer": "Thư của bạn lớn hơn kích thước thường được hệ thống email của bên thứ ba chấp nhận. Nếu bạn xác nhận đã gửi thư này, có nguy cơ nó sẽ bị hệ thống người nhận từ chối.",
"@messageWarningDialogWhenExceedMaximumFileSizeComposer": { "@messageWarningDialogWhenExceedMaximumFileSizeComposer": {
"warningMessageWhenExceedGenerallySizeInComposer": "Thư của bạn lớn hơn kích thước thường được hệ thống email của bên thứ ba chấp nhận. Nếu bạn xác nhận đã gửi thư này, có nguy cơ nó sẽ bị hệ thống người nhận từ chối.",
"@warningMessageWhenExceedGenerallySizeInComposer": {
"type": "text", "type": "text",
"placeholders_order": [], "placeholders_order": [],
"placeholders": {} "placeholders": {}
+16 -2
View File
@@ -3917,10 +3917,10 @@ class AppLocalizations {
); );
} }
String get messageWarningDialogWhenExceedMaximumFileSizeComposer { String get warningMessageWhenExceedGenerallySizeInComposer {
return Intl.message( return Intl.message(
'Your message is larger than the size generally accepted by third party email systems. If you confirm sending this mail, there is a risk that it gets rejected by your recipient system.', 'Your message is larger than the size generally accepted by third party email systems. If you confirm sending this mail, there is a risk that it gets rejected by your recipient system.',
name: 'messageWarningDialogWhenExceedMaximumFileSizeComposer', name: 'warningMessageWhenExceedGenerallySizeInComposer',
); );
} }
@@ -3930,4 +3930,18 @@ class AppLocalizations {
name: 'continueAction', name: 'continueAction',
); );
} }
String get thisFileCannotBePicked {
return Intl.message(
'This file cannot be picked.',
name: 'thisFileCannotBePicked',
);
}
String get loadingPleaseWait {
return Intl.message(
'Loading... Please wait!',
name: 'loadingPleaseWait',
);
}
} }
+1 -1
View File
@@ -8,7 +8,7 @@ import 'package:tmail_ui_user/main/localizations/app_localizations.dart';
class AppConfig { class AppConfig {
static const int limitCharToStartSearch = 3; static const int limitCharToStartSearch = 3;
static const int maximumMegabytesSizeFileAttachedInComposer = 10; static const int warningAttachmentFileSizeInMegabytes = 10;
static const String appDashboardConfigurationPath = "configurations/app_dashboard.json"; static const String appDashboardConfigurationPath = "configurations/app_dashboard.json";
static const String appFCMConfigurationPath = "configurations/env.fcm"; static const String appFCMConfigurationPath = "configurations/env.fcm";
+44 -11
View File
@@ -1,32 +1,65 @@
import 'package:equatable/equatable.dart'; import 'package:equatable/equatable.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:mime/mime.dart'; import 'package:mime/mime.dart';
class FileInfo with EquatableMixin { class FileInfo with EquatableMixin {
final String fileName; final String fileName;
final String filePath;
final int fileSize; final int fileSize;
final String? filePath;
final Uint8List? bytes; final Uint8List? bytes;
final Stream<List<int>>? readStream;
final String? type;
final bool? isInline;
final bool? isShared;
FileInfo(this.fileName, this.filePath, this.fileSize, {this.bytes}); FileInfo({
required this.fileName,
factory FileInfo.empty() { required this.fileSize,
return FileInfo('', '', 0); this.filePath,
} this.bytes,
this.readStream,
this.type,
this.isInline,
this.isShared,
});
factory FileInfo.fromBytes({ factory FileInfo.fromBytes({
required Uint8List bytes, required Uint8List bytes,
String? name, String? name,
int? size int? size,
String? type,
}) { }) {
return FileInfo(name ?? '', '', size ?? 0, bytes: bytes); return FileInfo(
fileName: name ?? '',
fileSize: size ?? 0,
bytes: bytes,
type: type
);
} }
String get fileExtension => fileName.split('.').last; String get fileExtension => fileName.split('.').last;
String get mimeType => lookupMimeType(kIsWeb ? fileName : filePath) ?? 'application/octet-stream'; String get mimeType {
if (type?.isNotEmpty == true) {
return type!;
} else if (filePath?.isNotEmpty == true){
final matchedType = lookupMimeType(filePath!, headerBytes: bytes) ?? 'application/octet-stream';
return matchedType;
} else {
final matchedType = lookupMimeType(fileName, headerBytes: bytes) ?? 'application/octet-stream';
return matchedType;
}
}
@override @override
List<Object?> get props => [fileName, filePath, fileSize, bytes]; List<Object?> get props => [
fileName,
filePath,
fileSize,
bytes,
readStream,
type,
isInline,
isShared,
];
} }
+16
View File
@@ -360,6 +360,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.0.0" version: "2.0.0"
desktop_drop:
dependency: "direct main"
description:
name: desktop_drop
sha256: d55a010fe46c8e8fcff4ea4b451a9ff84a162217bdb3b2a0aa1479776205e15d
url: "https://pub.dev"
source: hosted
version: "0.4.4"
device_info_plus: device_info_plus:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -990,6 +998,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.2.0" version: "3.2.0"
future_loading_dialog:
dependency: "direct main"
description:
name: future_loading_dialog
sha256: "2718b1a308db452da32ab9bca9ad496ff92b683e217add9e92cf50520f90537e"
url: "https://pub.dev"
source: hosted
version: "0.3.0"
get: get:
dependency: "direct main" dependency: "direct main"
description: description:
+4
View File
@@ -240,6 +240,10 @@ dependencies:
mime: 1.0.4 mime: 1.0.4
desktop_drop: 0.4.4
future_loading_dialog: 0.3.0
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:
sdk: flutter sdk: flutter
@@ -1,7 +1,7 @@
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:http_parser/http_parser.dart'; import 'package:http_parser/http_parser.dart';
import 'package:model/model.dart'; import 'package:model/email/attachment.dart';
import 'package:tmail_ui_user/features/email/presentation/extensions/attachment_extension.dart'; import 'package:tmail_ui_user/features/upload/domain/extensions/media_type_extension.dart';
void main() { void main() {
final attachmentA = Attachment( final attachmentA = Attachment(
@@ -34,7 +34,7 @@ void main() {
'WHEN perform call `attachmentA.isDisplayedPDFIcon()`\n' 'WHEN perform call `attachmentA.isDisplayedPDFIcon()`\n'
'SHOULD return true', 'SHOULD return true',
() { () {
bool result = attachmentA.isDisplayedPDFIcon; bool result = attachmentA.type!.validatePDFIcon(fileName: null);
expect(result, isTrue); expect(result, isTrue);
}); });
@@ -45,7 +45,7 @@ void main() {
'WHEN perform call `attachmentB.isDisplayedPDFIcon()`\n' 'WHEN perform call `attachmentB.isDisplayedPDFIcon()`\n'
'SHOULD return true', 'SHOULD return true',
() { () {
bool result = attachmentB.isDisplayedPDFIcon; bool result = attachmentB.type!.validatePDFIcon(fileName: 'attachmentB.pdf');
expect(result, isTrue); expect(result, isTrue);
}); });
@@ -56,7 +56,7 @@ void main() {
'WHEN perform call `attachmentC.isDisplayedPDFIcon()`\n' 'WHEN perform call `attachmentC.isDisplayedPDFIcon()`\n'
'SHOULD return false', 'SHOULD return false',
() { () {
bool result = attachmentC.isDisplayedPDFIcon; bool result = attachmentC.type!.validatePDFIcon(fileName: 'attachmentC.docx');
expect(result, isFalse); expect(result, isFalse);
}); });
@@ -67,7 +67,7 @@ void main() {
'WHEN perform call `attachmentD.isDisplayedPDFIcon()`\n' 'WHEN perform call `attachmentD.isDisplayedPDFIcon()`\n'
'SHOULD return false', 'SHOULD return false',
() { () {
bool result = attachmentD.isDisplayedPDFIcon; bool result = attachmentD.type!.validatePDFIcon(fileName: 'attachmentD.png');
expect(result, isFalse); expect(result, isFalse);
}); });
@@ -78,7 +78,7 @@ void main() {
'WHEN perform call `attachmentE.isDisplayedPDFIcon()`\n' 'WHEN perform call `attachmentE.isDisplayedPDFIcon()`\n'
'SHOULD return false', 'SHOULD return false',
() { () {
bool result = attachmentE.isDisplayedPDFIcon; bool result = attachmentE.type!.validatePDFIcon(fileName: 'attachmentE.pdf');
expect(result, isFalse); expect(result, isFalse);
}); });