TF-3084 Handle drop image to identity editor for public asset
This commit is contained in:
@@ -6,6 +6,7 @@ export 'presentation/extensions/url_extension.dart';
|
||||
export 'presentation/extensions/uri_extension.dart';
|
||||
export 'presentation/extensions/capitalize_extension.dart';
|
||||
export 'presentation/extensions/list_extensions.dart';
|
||||
export 'presentation/extensions/list_nullable_extensions.dart';
|
||||
export 'domain/extensions/datetime_extension.dart';
|
||||
export 'presentation/extensions/html_extension.dart';
|
||||
export 'presentation/extensions/compare_string_extension.dart';
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
|
||||
extension ListNullableExtensions<T> on List<T>? {
|
||||
bool get validateFilesTransfer => this?.any((type) => type == 'Files') ?? false;
|
||||
}
|
||||
@@ -385,7 +385,7 @@ class ComposerController extends BaseController with DragDropFileMixin implement
|
||||
_subscriptionOnDragEnter = html.window.onDragEnter.listen((event) {
|
||||
event.preventDefault();
|
||||
|
||||
if (_validateFilesTransfer(event.dataTransfer.types)) {
|
||||
if (event.dataTransfer.types.validateFilesTransfer) {
|
||||
mailboxDashBoardController.localFileDraggableAppState.value = DraggableAppState.active;
|
||||
}
|
||||
});
|
||||
@@ -393,7 +393,7 @@ class ComposerController extends BaseController with DragDropFileMixin implement
|
||||
_subscriptionOnDragOver = html.window.onDragOver.listen((event) {
|
||||
event.preventDefault();
|
||||
|
||||
if (_validateFilesTransfer(event.dataTransfer.types)) {
|
||||
if (event.dataTransfer.types.validateFilesTransfer) {
|
||||
mailboxDashBoardController.localFileDraggableAppState.value = DraggableAppState.active;
|
||||
}
|
||||
});
|
||||
@@ -401,7 +401,7 @@ class ComposerController extends BaseController with DragDropFileMixin implement
|
||||
_subscriptionOnDragLeave = html.window.onDragLeave.listen((event) {
|
||||
event.preventDefault();
|
||||
|
||||
if (_validateFilesTransfer(event.dataTransfer.types)) {
|
||||
if (event.dataTransfer.types.validateFilesTransfer) {
|
||||
mailboxDashBoardController.localFileDraggableAppState.value = DraggableAppState.inActive;
|
||||
}
|
||||
});
|
||||
@@ -409,16 +409,12 @@ class ComposerController extends BaseController with DragDropFileMixin implement
|
||||
_subscriptionOnDrop = html.window.onDrop.listen((event) {
|
||||
event.preventDefault();
|
||||
|
||||
if (_validateFilesTransfer(event.dataTransfer.types)) {
|
||||
if (event.dataTransfer.types.validateFilesTransfer) {
|
||||
mailboxDashBoardController.localFileDraggableAppState.value = DraggableAppState.inActive;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
bool _validateFilesTransfer(List<dynamic>? types) {
|
||||
return types?.any((type) => type == 'Files') ?? false;
|
||||
}
|
||||
|
||||
Future<void> _saveComposerCacheOnWebAction() async {
|
||||
_autoCreateEmailTag();
|
||||
|
||||
@@ -2130,7 +2126,7 @@ class ComposerController extends BaseController with DragDropFileMixin implement
|
||||
}
|
||||
|
||||
void handleOnDragEnterHtmlEditorWeb(List<dynamic>? types) {
|
||||
if (_validateFilesTransfer(types)) {
|
||||
if (types.validateFilesTransfer) {
|
||||
mailboxDashBoardController.localFileDraggableAppState.value = DraggableAppState.active;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ class LocalFileDropZoneWidget extends StatelessWidget {
|
||||
final ImagePaths imagePaths;
|
||||
final double? width;
|
||||
final double? height;
|
||||
final EdgeInsetsGeometry margin;
|
||||
final OnLocalFileDropZoneListener? onLocalFileDropZoneListener;
|
||||
|
||||
const LocalFileDropZoneWidget({
|
||||
@@ -21,6 +22,7 @@ class LocalFileDropZoneWidget extends StatelessWidget {
|
||||
required this.imagePaths,
|
||||
this.width,
|
||||
this.height,
|
||||
this.margin = DropZoneWidgetStyle.margin,
|
||||
this.onLocalFileDropZoneListener
|
||||
});
|
||||
|
||||
@@ -32,7 +34,7 @@ class LocalFileDropZoneWidget extends StatelessWidget {
|
||||
width: width,
|
||||
height: height,
|
||||
child: Padding(
|
||||
padding: DropZoneWidgetStyle.margin,
|
||||
padding: margin,
|
||||
child: DottedBorder(
|
||||
borderType: BorderType.RRect,
|
||||
radius: const Radius.circular(DropZoneWidgetStyle.radius),
|
||||
@@ -52,7 +54,7 @@ class LocalFileDropZoneWidget extends StatelessWidget {
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
SvgPicture.asset(imagePaths.icDropZoneIcon),
|
||||
Flexible(child: SvgPicture.asset(imagePaths.icDropZoneIcon)),
|
||||
const SizedBox(height: DropZoneWidgetStyle.space),
|
||||
Text(
|
||||
AppLocalizations.of(context).dropFileHereToAttachThem,
|
||||
|
||||
@@ -3,12 +3,15 @@ import 'dart:io';
|
||||
import 'dart:math' as math;
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:core/presentation/extensions/list_nullable_extensions.dart';
|
||||
import 'package:core/presentation/state/failure.dart';
|
||||
import 'package:core/presentation/state/success.dart';
|
||||
import 'package:core/presentation/utils/keyboard_utils.dart';
|
||||
import 'package:core/utils/app_logger.dart';
|
||||
import 'package:core/utils/file_utils.dart';
|
||||
import 'package:core/utils/platform_info.dart';
|
||||
import 'package:desktop_drop/desktop_drop.dart';
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/scheduler.dart';
|
||||
@@ -32,7 +35,7 @@ import 'package:rich_text_composer/views/commons/constants.dart';
|
||||
import 'package:tmail_ui_user/features/base/base_controller.dart';
|
||||
import 'package:tmail_ui_user/features/composer/presentation/controller/rich_text_mobile_tablet_controller.dart';
|
||||
import 'package:tmail_ui_user/features/composer/presentation/controller/rich_text_web_controller.dart';
|
||||
import 'package:tmail_ui_user/features/identity_creator/presentation/extesions/size_extension.dart';
|
||||
import 'package:tmail_ui_user/features/composer/presentation/mixin/drag_drog_file_mixin.dart';
|
||||
import 'package:tmail_ui_user/features/identity_creator/presentation/model/identity_creator_arguments.dart';
|
||||
import 'package:tmail_ui_user/features/identity_creator/presentation/utils/identity_creator_constants.dart';
|
||||
import 'package:tmail_ui_user/features/mailbox_creator/domain/model/verification/email_address_validator.dart';
|
||||
@@ -40,6 +43,7 @@ import 'package:tmail_ui_user/features/mailbox_creator/domain/model/verification
|
||||
import 'package:tmail_ui_user/features/mailbox_creator/domain/state/verify_name_view_state.dart';
|
||||
import 'package:tmail_ui_user/features/mailbox_creator/domain/usecases/verify_name_interactor.dart';
|
||||
import 'package:tmail_ui_user/features/mailbox_creator/presentation/extensions/validator_failure_extension.dart';
|
||||
import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/model/draggable_app_state.dart';
|
||||
import 'package:tmail_ui_user/features/manage_account/domain/model/create_new_identity_request.dart';
|
||||
import 'package:tmail_ui_user/features/manage_account/domain/model/edit_identity_request.dart';
|
||||
import 'package:tmail_ui_user/features/manage_account/domain/state/get_all_identities_state.dart';
|
||||
@@ -51,12 +55,16 @@ import 'package:tmail_ui_user/features/public_asset/domain/model/public_assets_i
|
||||
import 'package:tmail_ui_user/features/public_asset/presentation/model/public_asset_arguments.dart';
|
||||
import 'package:tmail_ui_user/features/public_asset/presentation/public_asset_bindings.dart';
|
||||
import 'package:tmail_ui_user/features/public_asset/presentation/public_asset_controller.dart';
|
||||
import 'package:tmail_ui_user/features/upload/domain/extensions/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_platform_file_extensions.dart';
|
||||
import 'package:tmail_ui_user/main/bindings/network/binding_tag.dart';
|
||||
import 'package:tmail_ui_user/main/error/capability_validator.dart';
|
||||
import 'package:tmail_ui_user/main/localizations/app_localizations.dart';
|
||||
import 'package:tmail_ui_user/main/routes/route_navigation.dart';
|
||||
import 'package:tmail_ui_user/main/universal_import/html_stub.dart' as html hide File;
|
||||
|
||||
class IdentityCreatorController extends BaseController {
|
||||
class IdentityCreatorController extends BaseController with DragDropFileMixin {
|
||||
|
||||
final VerifyNameInteractor _verifyNameInteractor;
|
||||
final GetAllIdentitiesInteractor _getAllIdentitiesInteractor;
|
||||
@@ -74,6 +82,7 @@ class IdentityCreatorController extends BaseController {
|
||||
final isDefaultIdentity = RxBool(false);
|
||||
final isDefaultIdentitySupported = RxBool(false);
|
||||
final isCompressingInlineImage = RxBool(false);
|
||||
final draggableAppState = DraggableAppState.inActive.obs;
|
||||
|
||||
final TextEditingController inputNameIdentityController = TextEditingController();
|
||||
final TextEditingController inputBccIdentityController = TextEditingController();
|
||||
@@ -83,6 +92,10 @@ class IdentityCreatorController extends BaseController {
|
||||
|
||||
RichTextMobileTabletController? richTextMobileTabletController;
|
||||
RichTextWebController? richTextWebController;
|
||||
StreamSubscription<html.Event>? _subscriptionOnDragEnter;
|
||||
StreamSubscription<html.Event>? _subscriptionOnDragOver;
|
||||
StreamSubscription<html.Event>? _subscriptionOnDragLeave;
|
||||
StreamSubscription<html.Event>? _subscriptionOnDrop;
|
||||
|
||||
String? _nameIdentity;
|
||||
String? _contentHtmlEditor;
|
||||
@@ -103,14 +116,23 @@ class IdentityCreatorController extends BaseController {
|
||||
|
||||
void updateContentHtmlEditor(String? text) => _contentHtmlEditor = text;
|
||||
|
||||
String? get contentHtmlEditor {
|
||||
String get contentHtmlEditor {
|
||||
if (_contentHtmlEditor != null) {
|
||||
return _contentHtmlEditor;
|
||||
return _contentHtmlEditor ?? '';
|
||||
} else {
|
||||
return arguments?.identity?.signatureAsString;
|
||||
return arguments?.identity?.signatureAsString ?? '';
|
||||
}
|
||||
}
|
||||
|
||||
int get maxSizeUploadByBytes {
|
||||
if (session == null || accountId == null) return 0;
|
||||
|
||||
return session!.getCapabilityProperties<CoreCapability>(
|
||||
accountId!,
|
||||
CapabilityIdentifier.jmapCore
|
||||
)?.maxSizeUpload?.value.toInt() ?? 0;
|
||||
}
|
||||
|
||||
IdentityCreatorController(
|
||||
this._verifyNameInteractor,
|
||||
this._getAllIdentitiesInteractor,
|
||||
@@ -138,10 +160,14 @@ class IdentityCreatorController extends BaseController {
|
||||
session = arguments!.session;
|
||||
identity = arguments!.identity;
|
||||
actionType.value = arguments!.actionType;
|
||||
if (actionType.value == IdentityActionType.create) {
|
||||
isLoadSignatureCompleted = true;
|
||||
}
|
||||
_checkDefaultIdentityIsSupported();
|
||||
_checkPublicAssetCapability();
|
||||
_setUpValueFromIdentity();
|
||||
_getAllIdentities();
|
||||
_triggerBrowserEventListener();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -152,6 +178,46 @@ class IdentityCreatorController extends BaseController {
|
||||
}
|
||||
}
|
||||
|
||||
void _triggerBrowserEventListener() {
|
||||
_subscriptionOnDragEnter = html.window.onDragEnter.listen((event) {
|
||||
event.preventDefault();
|
||||
|
||||
if (event.dataTransfer.types.validateFilesTransfer) {
|
||||
draggableAppState.value = DraggableAppState.active;
|
||||
}
|
||||
});
|
||||
|
||||
_subscriptionOnDragOver = html.window.onDragOver.listen((event) {
|
||||
event.preventDefault();
|
||||
|
||||
if (event.dataTransfer.types.validateFilesTransfer) {
|
||||
draggableAppState.value = DraggableAppState.active;
|
||||
}
|
||||
});
|
||||
|
||||
_subscriptionOnDragLeave = html.window.onDragLeave.listen((event) {
|
||||
event.preventDefault();
|
||||
|
||||
if (event.dataTransfer.types.validateFilesTransfer) {
|
||||
draggableAppState.value = DraggableAppState.inActive;
|
||||
}
|
||||
});
|
||||
|
||||
_subscriptionOnDrop = html.window.onDrop.listen((event) {
|
||||
event.preventDefault();
|
||||
|
||||
if (event.dataTransfer.types.validateFilesTransfer) {
|
||||
draggableAppState.value = DraggableAppState.inActive;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void handleOnDragEnterSignatureEditorWeb(List<dynamic>? types) {
|
||||
if (types.validateFilesTransfer) {
|
||||
draggableAppState.value = DraggableAppState.active;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void onClose() {
|
||||
log('IdentityCreatorController::onClose():');
|
||||
@@ -170,6 +236,10 @@ class IdentityCreatorController extends BaseController {
|
||||
}
|
||||
Get.delete<PublicAssetController>(tag: BindingTag.publicAssetBindingsTag);
|
||||
publicAssetController = null;
|
||||
_subscriptionOnDragEnter?.cancel();
|
||||
_subscriptionOnDragOver?.cancel();
|
||||
_subscriptionOnDragLeave?.cancel();
|
||||
_subscriptionOnDrop?.cancel();
|
||||
super.onClose();
|
||||
}
|
||||
|
||||
@@ -177,7 +247,7 @@ class IdentityCreatorController extends BaseController {
|
||||
void handleSuccessViewState(Success success) {
|
||||
super.handleSuccessViewState(success);
|
||||
if (success is GetAllIdentitiesSuccess) {
|
||||
_getALlIdentitiesSuccess(success);
|
||||
_getAllIdentitiesSuccess(success);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -185,7 +255,7 @@ class IdentityCreatorController extends BaseController {
|
||||
void handleFailureViewState(Failure failure) {
|
||||
super.handleFailureViewState(failure);
|
||||
if (failure is GetAllIdentitiesFailure) {
|
||||
_getALlIdentitiesFailure(failure);
|
||||
_getAllIdentitiesFailure(failure);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -218,7 +288,7 @@ class IdentityCreatorController extends BaseController {
|
||||
}
|
||||
}
|
||||
|
||||
void _getALlIdentitiesSuccess(GetAllIdentitiesSuccess success) {
|
||||
void _getAllIdentitiesSuccess(GetAllIdentitiesSuccess success) {
|
||||
if (success.identities?.isNotEmpty == true) {
|
||||
listEmailAddressDefault.value = success.identities!
|
||||
.map((identity) => identity.toEmailAddressNoName())
|
||||
@@ -236,7 +306,7 @@ class IdentityCreatorController extends BaseController {
|
||||
}
|
||||
}
|
||||
|
||||
void _getALlIdentitiesFailure(GetAllIdentitiesFailure failure) {
|
||||
void _getAllIdentitiesFailure(GetAllIdentitiesFailure failure) {
|
||||
_setDefaultEmailAddressList();
|
||||
}
|
||||
|
||||
@@ -339,11 +409,11 @@ class IdentityCreatorController extends BaseController {
|
||||
bccOfIdentity.value = newEmailAddress;
|
||||
}
|
||||
|
||||
Future<String?> _getSignatureHtmlText() async {
|
||||
Future<String> _getSignatureHtmlText() async {
|
||||
if (PlatformInfo.isWeb) {
|
||||
return richTextWebController?.editorController.getText();
|
||||
return (await richTextWebController?.editorController.getText()) ?? '';
|
||||
} else {
|
||||
return richTextMobileTabletController?.richTextController.htmlEditorApi?.getText();
|
||||
return (await richTextMobileTabletController?.richTextController.htmlEditorApi?.getText()) ?? '';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -380,7 +450,7 @@ class IdentityCreatorController extends BaseController {
|
||||
: null;
|
||||
|
||||
final publicAssetsInIdentityArguments = PublicAssetsInIdentityArguments(
|
||||
htmlSignature: signatureHtmlText ?? '',
|
||||
htmlSignature: signatureHtmlText,
|
||||
preExistingPublicAssetIds: List.from(publicAssetController?.preExistingPublicAssetIds ?? []),
|
||||
newlyPickedPublicAssetIds: List.from(publicAssetController?.newlyPickedPublicAssetIds ?? []),
|
||||
);
|
||||
@@ -390,7 +460,7 @@ class IdentityCreatorController extends BaseController {
|
||||
email: emailOfIdentity.value?.email,
|
||||
replyTo: replyToAddress,
|
||||
bcc: bccAddress,
|
||||
htmlSignature: Signature(signatureHtmlText ?? ''),
|
||||
htmlSignature: Signature(signatureHtmlText),
|
||||
sortOrder: sortOrder);
|
||||
|
||||
final generateCreateId = Id(uuid.v1());
|
||||
@@ -470,6 +540,8 @@ class IdentityCreatorController extends BaseController {
|
||||
}
|
||||
|
||||
void clearFocusEditor(BuildContext context) {
|
||||
inputNameIdentityFocusNode.unfocus();
|
||||
inputBccIdentityFocusNode.unfocus();
|
||||
if (PlatformInfo.isMobile) {
|
||||
richTextMobileTabletController?.richTextController.htmlEditorApi?.unfocus();
|
||||
}
|
||||
@@ -549,47 +621,31 @@ class IdentityCreatorController extends BaseController {
|
||||
logError("IdentityCreatorController::pickImage: context is unmounted");
|
||||
}
|
||||
}
|
||||
|
||||
bool _isExceedMaxSizeInlineImage(int fileSize) =>
|
||||
fileSize > IdentityCreatorConstants.maxKBSizeIdentityInlineImage.toBytes;
|
||||
|
||||
bool _isExceedMaxUploadSize(int fileSize) {
|
||||
final coreCapability = session?.getCapabilityProperties<CoreCapability>(
|
||||
accountId!,
|
||||
CapabilityIdentifier.jmapCore
|
||||
);
|
||||
|
||||
int maxUploadSize = coreCapability?.maxSizeUpload?.value.toInt() ?? 0;
|
||||
|
||||
return fileSize > maxUploadSize;
|
||||
return fileSize > maxSizeUploadByBytes;
|
||||
}
|
||||
|
||||
void _insertInlineImage(
|
||||
Future<void> _insertInlineImage(
|
||||
BuildContext context,
|
||||
PlatformFile platformFile,
|
||||
int maxWidth
|
||||
int maxWidth,
|
||||
{PlatformFile? compressedFile}
|
||||
) async {
|
||||
final PlatformFile file;
|
||||
try {
|
||||
isCompressingInlineImage.value = true;
|
||||
file = await _compressImage(platformFile, maxWidth);
|
||||
isCompressingInlineImage.value = false;
|
||||
} catch (e) {
|
||||
logError("IdentityCreatorController::_insertInlineImage: compress image error: $e");
|
||||
isCompressingInlineImage.value = false;
|
||||
if (context.mounted) {
|
||||
appToast.showToastErrorMessage(
|
||||
context,
|
||||
AppLocalizations.of(context).cannotCompressInlineImage);
|
||||
}
|
||||
return;
|
||||
final PlatformFile? file;
|
||||
if (compressedFile != null) {
|
||||
file = compressedFile;
|
||||
} else {
|
||||
file = await _compressFileAction(context, originalFile: platformFile, maxWidth: maxWidth);
|
||||
}
|
||||
if (_isExceedMaxSizeInlineImage(file.size) || _isExceedMaxUploadSize(file.size)) {
|
||||
if (file == null) return;
|
||||
|
||||
if (_isExceedMaxUploadSize(file.size)) {
|
||||
if (context.mounted) {
|
||||
appToast.showToastErrorMessage(
|
||||
context,
|
||||
AppLocalizations.of(context).pleaseChooseAnImageSizeCorrectly(
|
||||
IdentityCreatorConstants.maxKBSizeIdentityInlineImage
|
||||
maxSizeUploadByBytes
|
||||
)
|
||||
);
|
||||
} else {
|
||||
@@ -613,8 +669,32 @@ class IdentityCreatorController extends BaseController {
|
||||
}
|
||||
}
|
||||
|
||||
Future<PlatformFile?> _compressFileAction(
|
||||
BuildContext context,
|
||||
{
|
||||
required PlatformFile originalFile,
|
||||
required int maxWidth
|
||||
}
|
||||
) async {
|
||||
try {
|
||||
isCompressingInlineImage.value = true;
|
||||
final compressedFile = await _compressImage(originalFile, maxWidth);
|
||||
isCompressingInlineImage.value = false;
|
||||
return compressedFile;
|
||||
} catch (e) {
|
||||
logError("$runtimeType::_compressFileAction: compress image error: $e");
|
||||
isCompressingInlineImage.value = false;
|
||||
if (context.mounted) {
|
||||
appToast.showToastErrorMessage(
|
||||
context,
|
||||
AppLocalizations.of(context).cannotCompressInlineImage);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Future<PlatformFile> _compressImage(PlatformFile originalFile, int maxWidthCompressedImage) async {
|
||||
if (originalFile.size <= IdentityCreatorConstants.maxKBSizeIdentityInlineImage.toBytes) {
|
||||
if (originalFile.size <= maxSizeUploadByBytes) {
|
||||
return originalFile;
|
||||
}
|
||||
|
||||
@@ -701,4 +781,58 @@ class IdentityCreatorController extends BaseController {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void onLocalFileDropZoneListener({
|
||||
required BuildContext context,
|
||||
required DropDoneDetails details,
|
||||
required double maxWidth
|
||||
}) async {
|
||||
try {
|
||||
clearFocusEditor(context);
|
||||
|
||||
final listFileInfo = await onDragDone(context: context, details: details);
|
||||
final listImages = listFileInfo.listInlineFiles;
|
||||
|
||||
if (listImages.isEmpty && listFileInfo.isNotEmpty && context.mounted) {
|
||||
appToast.showToastErrorMessage(
|
||||
context,
|
||||
AppLocalizations.of(context).canNotUploadFileToSignature
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
final listCompressedImages = await Future.wait(
|
||||
listImages.map((fileInfo) => _compressFileAction(
|
||||
context,
|
||||
originalFile: fileInfo.toPlatformFile(),
|
||||
maxWidth: maxWidth.toInt()))
|
||||
).then((listPlatformFiles) => listPlatformFiles.whereNotNull().toList());
|
||||
|
||||
if (_isExceedMaxUploadSize(listCompressedImages.totalFilesSize)) {
|
||||
if (context.mounted) {
|
||||
appToast.showToastErrorMessage(
|
||||
context,
|
||||
AppLocalizations.of(context).pleaseChooseAnImageSizeCorrectly(
|
||||
maxSizeUploadByBytes
|
||||
)
|
||||
);
|
||||
} else {
|
||||
logError("IdentityCreatorController::onLocalFileDropZoneListener: context is unmounted");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
await Future.forEach(
|
||||
listCompressedImages,
|
||||
(platformFile) => _insertInlineImage(
|
||||
context,
|
||||
platformFile,
|
||||
maxWidth.toInt(),
|
||||
compressedFile: platformFile
|
||||
)
|
||||
);
|
||||
} catch (e) {
|
||||
logError("IdentityCreatorController::onLocalFileDropZoneListener: error: $e");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -17,14 +17,16 @@ import 'package:pointer_interceptor/pointer_interceptor.dart';
|
||||
import 'package:rich_text_composer/rich_text_composer.dart';
|
||||
import 'package:rich_text_composer/views/widgets/rich_text_keyboard_toolbar.dart';
|
||||
import 'package:tmail_ui_user/features/composer/presentation/mixin/rich_text_button_mixin.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/toolbar_rich_text_builder.dart';
|
||||
import 'package:tmail_ui_user/features/identity_creator/presentation/identity_creator_controller.dart';
|
||||
import 'package:tmail_ui_user/features/identity_creator/presentation/widgets/compress_image_loading_bar_widget.dart';
|
||||
import 'package:tmail_ui_user/features/identity_creator/presentation/widgets/identity_drop_list_field_builder.dart';
|
||||
import 'package:tmail_ui_user/features/identity_creator/presentation/widgets/identity_field_no_editable_builder.dart';
|
||||
import 'package:tmail_ui_user/features/identity_creator/presentation/widgets/identity_input_field_builder.dart';
|
||||
import 'package:tmail_ui_user/features/identity_creator/presentation/widgets/identity_input_with_drop_list_field_builder.dart';
|
||||
import 'package:tmail_ui_user/features/identity_creator/presentation/widgets/insert_image_loading_indicator.dart';
|
||||
import 'package:tmail_ui_user/features/identity_creator/presentation/widgets/set_default_identity_checkbox_builder.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/presentation/model/identity_action_type.dart';
|
||||
import 'package:tmail_ui_user/main/localizations/app_localizations.dart';
|
||||
@@ -40,87 +42,126 @@ class IdentityCreatorView extends GetWidget<IdentityCreatorController>
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Widget bodyCreatorView = SingleChildScrollView(
|
||||
controller: controller.scrollController,
|
||||
physics: const ClampingScrollPhysics(),
|
||||
child: Padding(
|
||||
padding: const EdgeInsetsDirectional.symmetric(vertical: 12, horizontal: 24),
|
||||
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||
Obx(() => IdentityInputFieldBuilder(
|
||||
AppLocalizations.of(context).name,
|
||||
controller.errorNameIdentity.value,
|
||||
AppLocalizations.of(context).required,
|
||||
editingController: controller.inputNameIdentityController,
|
||||
focusNode: controller.inputNameIdentityFocusNode,
|
||||
isMandatory: true,
|
||||
onChangeInputNameAction: (value) => controller.updateNameIdentity(context, value)
|
||||
)),
|
||||
const SizedBox(height: 24),
|
||||
Obx(() {
|
||||
if (controller.actionType.value == IdentityActionType.create) {
|
||||
return IdentityDropListFieldBuilder(
|
||||
controller.imagePaths,
|
||||
AppLocalizations.of(context).email.inCaps,
|
||||
controller.emailOfIdentity.value,
|
||||
controller.listEmailAddressDefault,
|
||||
onSelectItemDropList: (emailAddress) => controller.updateEmailOfIdentity(context, emailAddress)
|
||||
);
|
||||
} else {
|
||||
return IdentityFieldNoEditableBuilder(
|
||||
AppLocalizations.of(context).email.inCaps,
|
||||
controller.emailOfIdentity.value
|
||||
);
|
||||
}
|
||||
}),
|
||||
const SizedBox(height: 24),
|
||||
Obx(() => IdentityDropListFieldBuilder(
|
||||
controller.imagePaths,
|
||||
AppLocalizations.of(context).reply_to,
|
||||
controller.replyToOfIdentity.value,
|
||||
controller.listEmailAddressOfReplyTo,
|
||||
onSelectItemDropList: (emailAddress) => controller.updaterReplyToOfIdentity(context, emailAddress)
|
||||
)),
|
||||
const SizedBox(height: 24),
|
||||
Obx(() => IdentityInputWithDropListFieldBuilder(
|
||||
AppLocalizations.of(context).bcc_to,
|
||||
controller.errorBccIdentity.value,
|
||||
controller.inputBccIdentityController,
|
||||
focusNode: controller.inputBccIdentityFocusNode,
|
||||
onSelectedSuggestionAction: (newEmailAddress) {
|
||||
controller.inputBccIdentityController.text = newEmailAddress?.email ?? '';
|
||||
controller.updateBccOfIdentity(newEmailAddress);
|
||||
},
|
||||
onChangeInputSuggestionAction: (pattern) {
|
||||
controller.validateInputBccAddress(context, pattern);
|
||||
if (pattern == null || pattern.trim().isEmpty) {
|
||||
controller.updateBccOfIdentity(null);
|
||||
Widget bodyCreatorView = NotificationListener<ScrollNotification>(
|
||||
onNotification: (scrollInfo) {
|
||||
if (scrollInfo is ScrollEndNotification &&
|
||||
scrollInfo.metrics.pixels == scrollInfo.metrics.maxScrollExtent) {
|
||||
controller.clearFocusEditor(context);
|
||||
}
|
||||
return false;
|
||||
},
|
||||
child: SingleChildScrollView(
|
||||
controller: controller.scrollController,
|
||||
physics: const ClampingScrollPhysics(),
|
||||
child: Padding(
|
||||
padding: const EdgeInsetsDirectional.symmetric(vertical: 12, horizontal: 24),
|
||||
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||
Obx(() => IdentityInputFieldBuilder(
|
||||
AppLocalizations.of(context).name,
|
||||
controller.errorNameIdentity.value,
|
||||
AppLocalizations.of(context).required,
|
||||
editingController: controller.inputNameIdentityController,
|
||||
focusNode: controller.inputNameIdentityFocusNode,
|
||||
isMandatory: true,
|
||||
onChangeInputNameAction: (value) => controller.updateNameIdentity(context, value)
|
||||
)),
|
||||
const SizedBox(height: 24),
|
||||
Obx(() {
|
||||
if (controller.actionType.value == IdentityActionType.create) {
|
||||
return IdentityDropListFieldBuilder(
|
||||
controller.imagePaths,
|
||||
AppLocalizations.of(context).email.inCaps,
|
||||
controller.emailOfIdentity.value,
|
||||
controller.listEmailAddressDefault,
|
||||
onSelectItemDropList: (emailAddress) => controller.updateEmailOfIdentity(context, emailAddress)
|
||||
);
|
||||
} else {
|
||||
controller.updateBccOfIdentity(EmailAddress(null, pattern));
|
||||
return IdentityFieldNoEditableBuilder(
|
||||
AppLocalizations.of(context).email.inCaps,
|
||||
controller.emailOfIdentity.value
|
||||
);
|
||||
}
|
||||
},
|
||||
onSuggestionCallbackAction: controller.getSuggestionEmailAddress
|
||||
)),
|
||||
const SizedBox(height: 32),
|
||||
Text(AppLocalizations.of(context).signature,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.normal,
|
||||
fontSize: 14,
|
||||
color: AppColor.colorContentEmail,
|
||||
}),
|
||||
const SizedBox(height: 24),
|
||||
Obx(() => IdentityDropListFieldBuilder(
|
||||
controller.imagePaths,
|
||||
AppLocalizations.of(context).reply_to,
|
||||
controller.replyToOfIdentity.value,
|
||||
controller.listEmailAddressOfReplyTo,
|
||||
onSelectItemDropList: (emailAddress) => controller.updaterReplyToOfIdentity(context, emailAddress)
|
||||
)),
|
||||
const SizedBox(height: 24),
|
||||
Obx(() => IdentityInputWithDropListFieldBuilder(
|
||||
AppLocalizations.of(context).bcc_to,
|
||||
controller.errorBccIdentity.value,
|
||||
controller.inputBccIdentityController,
|
||||
focusNode: controller.inputBccIdentityFocusNode,
|
||||
onSelectedSuggestionAction: (newEmailAddress) {
|
||||
controller.inputBccIdentityController.text = newEmailAddress?.email ?? '';
|
||||
controller.updateBccOfIdentity(newEmailAddress);
|
||||
},
|
||||
onChangeInputSuggestionAction: (pattern) {
|
||||
controller.validateInputBccAddress(context, pattern);
|
||||
if (pattern == null || pattern.trim().isEmpty) {
|
||||
controller.updateBccOfIdentity(null);
|
||||
} else {
|
||||
controller.updateBccOfIdentity(EmailAddress(null, pattern));
|
||||
}
|
||||
},
|
||||
onSuggestionCallbackAction: controller.getSuggestionEmailAddress
|
||||
)),
|
||||
const SizedBox(height: 32),
|
||||
Text(AppLocalizations.of(context).signature,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.normal,
|
||||
fontSize: 14,
|
||||
color: AppColor.colorContentEmail,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: AppColor.colorInputBorderCreateMailbox),
|
||||
const SizedBox(height: 8),
|
||||
LayoutBuilder(
|
||||
builder: (context, constraintsEditor) {
|
||||
return Stack(
|
||||
children: [
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: AppColor.colorInputBorderCreateMailbox),
|
||||
),
|
||||
padding: const EdgeInsetsDirectional.all(16),
|
||||
child: _buildSignatureHtmlTemplate(context),
|
||||
),
|
||||
Obx(() {
|
||||
if (controller.draggableAppState.value == DraggableAppState.inActive) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
return Positioned.fill(
|
||||
child: PointerInterceptor(
|
||||
child: LocalFileDropZoneWidget(
|
||||
imagePaths: controller.imagePaths,
|
||||
width: constraintsEditor.maxWidth,
|
||||
height: constraintsEditor.maxHeight,
|
||||
margin: EdgeInsets.zero,
|
||||
onLocalFileDropZoneListener: (details) =>
|
||||
controller.onLocalFileDropZoneListener(
|
||||
context: context,
|
||||
details: details,
|
||||
maxWidth: constraintsEditor.maxWidth,
|
||||
),
|
||||
)
|
||||
),
|
||||
);
|
||||
})
|
||||
],
|
||||
);
|
||||
}
|
||||
),
|
||||
padding: const EdgeInsetsDirectional.all(16),
|
||||
child: _buildSignatureHtmlTemplate(context),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
if (controller.isMobile(context))
|
||||
_buildActionButtonMobile(context)
|
||||
]),
|
||||
const SizedBox(height: 12),
|
||||
if (controller.isMobile(context))
|
||||
_buildActionButtonMobile(context)
|
||||
]),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -400,13 +441,9 @@ class IdentityCreatorView extends GetWidget<IdentityCreatorController>
|
||||
}
|
||||
|
||||
Widget _buildSignatureHtmlTemplate(BuildContext context) {
|
||||
final htmlEditor = PlatformInfo.isWeb
|
||||
? _buildHtmlEditorWeb(context, controller.contentHtmlEditor ?? '')
|
||||
: _buildHtmlEditor(context, initialContent: controller.contentHtmlEditor ?? '');
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
if (PlatformInfo.isWeb)
|
||||
if (PlatformInfo.isWeb) {
|
||||
return Column(
|
||||
children: [
|
||||
ToolbarRichTextWebBuilder(
|
||||
richTextWebController: controller.richTextWebController!,
|
||||
padding: const EdgeInsets.only(bottom: 12),
|
||||
@@ -424,18 +461,34 @@ class IdentityCreatorView extends GetWidget<IdentityCreatorController>
|
||||
),
|
||||
]
|
||||
),
|
||||
Stack(
|
||||
children: [
|
||||
htmlEditor,
|
||||
Obx(() => Center(
|
||||
child: CompressImageLoadingBarWidget(
|
||||
isCompressing: controller.isCompressingInlineImage.value,
|
||||
),
|
||||
))
|
||||
]
|
||||
),
|
||||
],
|
||||
);
|
||||
Stack(
|
||||
children: [
|
||||
_buildHtmlEditorWeb(
|
||||
context,
|
||||
controller.contentHtmlEditor),
|
||||
Obx(() {
|
||||
bool isInserting = controller.publicAssetController?.isUploading.isTrue == true
|
||||
|| controller.isCompressingInlineImage.isTrue;
|
||||
return InsertImageLoadingIndicator(isInserting: isInserting);
|
||||
})
|
||||
]
|
||||
),
|
||||
],
|
||||
);
|
||||
} else {
|
||||
return Stack(
|
||||
children: [
|
||||
_buildHtmlEditor(
|
||||
context,
|
||||
initialContent: controller.contentHtmlEditor),
|
||||
Obx(() {
|
||||
bool isInserting = controller.publicAssetController?.isUploading.isTrue == true
|
||||
|| controller.isCompressingInlineImage.isTrue;
|
||||
return InsertImageLoadingIndicator(isInserting: isInserting);
|
||||
})
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildHtmlEditorWeb(BuildContext context, String initContent) {
|
||||
@@ -447,6 +500,7 @@ class IdentityCreatorView extends GetWidget<IdentityCreatorController>
|
||||
hint: '',
|
||||
darkMode: false,
|
||||
initialText: initContent.isEmpty ? null : initContent,
|
||||
disableDragAndDrop: true,
|
||||
customBodyCssStyle: HtmlUtils.customCssStyleHtmlEditor(direction: AppUtils.getCurrentDirection(context)),
|
||||
),
|
||||
htmlToolbarOptions: const html_editor_browser.HtmlToolbarOptions(
|
||||
@@ -463,9 +517,11 @@ class IdentityCreatorView extends GetWidget<IdentityCreatorController>
|
||||
}
|
||||
},
|
||||
onInit: () {
|
||||
controller.richTextWebController?.editorController.setOnDragDropEvent();
|
||||
controller.richTextWebController?.editorController.setFullScreen();
|
||||
controller.updateContentHtmlEditor(initContent);
|
||||
}, onFocus: () {
|
||||
},
|
||||
onFocus: () {
|
||||
FocusManager.instance.primaryFocus?.unfocus();
|
||||
Future.delayed(const Duration(milliseconds: 500), () {
|
||||
controller.richTextWebController?.editorController.setFocus();
|
||||
@@ -473,7 +529,9 @@ class IdentityCreatorView extends GetWidget<IdentityCreatorController>
|
||||
controller.richTextWebController?.closeAllMenuPopup();
|
||||
},
|
||||
onChangeSelection: controller.richTextWebController?.onEditorSettingsChange,
|
||||
onChangeCodeview: controller.updateContentHtmlEditor
|
||||
onChangeCodeview: controller.updateContentHtmlEditor,
|
||||
onDragEnter: controller.handleOnDragEnterSignatureEditorWeb,
|
||||
onDragLeave: (_) {}
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
|
||||
class IdentityCreatorConstants {
|
||||
static const int maxKBSizeIdentityInlineImage = 128; // Kilobyte
|
||||
static const double maxWidthInlineImageDesktop = 800; // Pixel
|
||||
static const double maxWidthInlineImageOther = 700; // Pixel
|
||||
static const String prefixCompressedInlineImageTemp = 'compressed_';
|
||||
static const int qualityCompressedInlineImage = 50; // 0 - 100
|
||||
static const int qualityCompressedInlineImage = 80; // 0 - 100
|
||||
}
|
||||
-23
@@ -1,23 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:tmail_ui_user/features/base/widget/circle_loading_widget.dart';
|
||||
|
||||
class CompressImageLoadingBarWidget extends StatelessWidget {
|
||||
|
||||
final bool isCompressing;
|
||||
final EdgeInsetsGeometry? padding;
|
||||
|
||||
const CompressImageLoadingBarWidget({
|
||||
super.key,
|
||||
required this.isCompressing,
|
||||
this.padding,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (isCompressing) {
|
||||
return CircleLoadingWidget(padding: padding);
|
||||
} else {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
}
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
|
||||
import 'package:core/presentation/extensions/color_extension.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class InsertImageLoadingIndicator extends StatelessWidget {
|
||||
|
||||
final bool isInserting;
|
||||
|
||||
const InsertImageLoadingIndicator({
|
||||
super.key,
|
||||
required this.isInserting
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (isInserting) {
|
||||
return const Align(
|
||||
alignment: Alignment.topCenter,
|
||||
child: CircularProgressIndicator(
|
||||
color: AppColor.primaryColor
|
||||
)
|
||||
);
|
||||
} else {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,9 @@ class CreatePublicAssetSuccessState extends UIState {
|
||||
CreatePublicAssetSuccessState(this.publicAsset);
|
||||
|
||||
final PublicAsset publicAsset;
|
||||
|
||||
@override
|
||||
List<Object?> get props => [publicAsset];
|
||||
}
|
||||
|
||||
class CreatePublicAssetFailureState extends FeatureFailure {
|
||||
|
||||
@@ -47,9 +47,8 @@ class PublicAssetController extends BaseController {
|
||||
List<PublicAssetId> newlyPickedPublicAssetIds = [];
|
||||
final _publicAssetStreamGroup = StreamGroup<Either<Failure, Success>>();
|
||||
StreamSubscription<Either<Failure, Success>>? _publicAssetStreamSubscription;
|
||||
final isUploading = false.obs;
|
||||
|
||||
StreamSubscription<Either<Failure, Success>>? get publicAssetStreamSubscription
|
||||
=> _publicAssetStreamSubscription;
|
||||
Session? get session => arguments?.session;
|
||||
AccountId? get accountId => arguments?.accountId;
|
||||
Identity? get identity => arguments?.identity;
|
||||
@@ -57,7 +56,7 @@ class PublicAssetController extends BaseController {
|
||||
void _handleUploadState(Either<Failure, Success> uploadState) {
|
||||
log('PublicAssetController::handleUploadState::${uploadState.runtimeType}');
|
||||
uploadState.fold(
|
||||
(failure) {},
|
||||
(failure) => consumeState(Stream.value(Left(CreatePublicAssetFailureState()))),
|
||||
(success) {
|
||||
log('PublicAssetController::handleUploadState::success::$success');
|
||||
if (success is SuccessAttachmentUploadState) {
|
||||
@@ -71,7 +70,10 @@ class PublicAssetController extends BaseController {
|
||||
final blobId = success.attachment.blobId;
|
||||
if (session == null
|
||||
|| accountId == null
|
||||
|| blobId == null) return;
|
||||
|| blobId == null) {
|
||||
consumeState(Stream.value(Left(CreatePublicAssetFailureState())));
|
||||
return;
|
||||
}
|
||||
|
||||
consumeState(_createPublicAssetInteractor.execute(
|
||||
session!,
|
||||
@@ -108,17 +110,27 @@ class PublicAssetController extends BaseController {
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void handleFailureViewState(Failure failure) {
|
||||
super.handleFailureViewState(failure);
|
||||
if (failure is CreatePublicAssetFailureState) {
|
||||
isUploading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _handleUploadAttachmentSuccess(UploadAttachmentSuccess success) async {
|
||||
await _publicAssetStreamGroup.add(success.uploadAttachment.progressState);
|
||||
}
|
||||
|
||||
void _handleCreatePublicAssetSuccessState(CreatePublicAssetSuccessState success) {
|
||||
isUploading.value = false;
|
||||
final publicAsset = success.publicAsset;
|
||||
if (publicAsset.id == null || publicAsset.publicURI == null) return;
|
||||
|
||||
newlyPickedPublicAssetIds.add(publicAsset.id!);
|
||||
final imageTag = '<img '
|
||||
'src="${publicAsset.publicURI!}" '
|
||||
'style="max-width: 100%" '
|
||||
'public-asset-id="${publicAsset.id!.value}">';
|
||||
if (PlatformInfo.isWeb) {
|
||||
Get
|
||||
@@ -140,6 +152,7 @@ class PublicAssetController extends BaseController {
|
||||
}
|
||||
|
||||
void uploadFileToBlob(PlatformFile platformFile) {
|
||||
isUploading.value = true;
|
||||
_uploadFileToBlobAction(platformFile);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
import 'package:model/upload/file_info.dart';
|
||||
import 'package:tmail_ui_user/features/upload/domain/model/mobile_file_upload.dart';
|
||||
|
||||
@@ -14,4 +15,12 @@ extension FileInfoExtension on FileInfo {
|
||||
type: type,
|
||||
isInline: true,
|
||||
isShared: isShared);
|
||||
|
||||
PlatformFile toPlatformFile() => PlatformFile(
|
||||
name: fileName,
|
||||
path: filePath,
|
||||
size: fileSize,
|
||||
bytes: bytes,
|
||||
readStream: readStream,
|
||||
);
|
||||
}
|
||||
@@ -1,7 +1,10 @@
|
||||
|
||||
import 'package:model/upload/file_info.dart';
|
||||
|
||||
extension ListFileInfoExtension on List<FileInfo> {
|
||||
List<int> get listSize => map((file) => file.fileSize).toList();
|
||||
|
||||
num get totalSize => listSize.isEmpty ? 0 : listSize.reduce((sum, size) => sum + size);
|
||||
|
||||
List<FileInfo> get listInlineFiles => where((fileInfo) => fileInfo.isInline == true).toList();
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
|
||||
extension ListPlatformFileExtensions on List<PlatformFile> {
|
||||
int get totalFilesSize => fold<int>(0, (sum, file) => sum + file.size);
|
||||
}
|
||||
@@ -4180,4 +4180,11 @@ class AppLocalizations {
|
||||
'Mark as spam failed',
|
||||
name: 'markAsSpamFailed');
|
||||
}
|
||||
|
||||
String get canNotUploadFileToSignature {
|
||||
return Intl.message(
|
||||
'Can not upload this file to signature',
|
||||
name: 'canNotUploadFileToSignature'
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user