TF-3416 Handle download attachment in EML previewer on mobile
This commit is contained in:
@@ -10,4 +10,7 @@ class Constant {
|
|||||||
static const textVCardMimeType = 'text/x-vcard';
|
static const textVCardMimeType = 'text/x-vcard';
|
||||||
static const textPlainMimeType = 'text/plain';
|
static const textPlainMimeType = 'text/plain';
|
||||||
static const emlMimeType = 'message/rfc822';
|
static const emlMimeType = 'message/rfc822';
|
||||||
|
static const mailtoScheme = 'mailto';
|
||||||
|
static const attachmentScheme = 'attachment';
|
||||||
|
static const emlPreviewerScheme = 'eml-previewer';
|
||||||
}
|
}
|
||||||
@@ -1,12 +1,12 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:math' as math;
|
import 'dart:math' as math;
|
||||||
|
|
||||||
|
import 'package:core/data/constants/constant.dart';
|
||||||
import 'package:core/presentation/views/loading/cupertino_loading_widget.dart';
|
import 'package:core/presentation/views/loading/cupertino_loading_widget.dart';
|
||||||
import 'package:core/utils/app_logger.dart';
|
import 'package:core/utils/app_logger.dart';
|
||||||
import 'package:core/utils/html/html_interaction.dart';
|
import 'package:core/utils/html/html_interaction.dart';
|
||||||
import 'package:core/utils/html/html_utils.dart';
|
import 'package:core/utils/html/html_utils.dart';
|
||||||
import 'package:core/utils/platform_info.dart';
|
import 'package:core/utils/platform_info.dart';
|
||||||
import 'package:core/utils/preview_eml_file_utils.dart';
|
|
||||||
import 'package:flutter/cupertino.dart';
|
import 'package:flutter/cupertino.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/gestures.dart';
|
import 'package:flutter/gestures.dart';
|
||||||
@@ -18,6 +18,7 @@ typedef OnScrollHorizontalEndAction = Function(bool leftDirection);
|
|||||||
typedef OnLoadWidthHtmlViewerAction = Function(bool isScrollPageViewActivated);
|
typedef OnLoadWidthHtmlViewerAction = Function(bool isScrollPageViewActivated);
|
||||||
typedef OnMailtoDelegateAction = Future<void> Function(Uri? uri);
|
typedef OnMailtoDelegateAction = Future<void> Function(Uri? uri);
|
||||||
typedef OnPreviewEMLDelegateAction = Future<void> Function(Uri? uri);
|
typedef OnPreviewEMLDelegateAction = Future<void> Function(Uri? uri);
|
||||||
|
typedef OnDownloadAttachmentDelegateAction = Future<void> Function(Uri? uri);
|
||||||
|
|
||||||
class HtmlContentViewer extends StatefulWidget {
|
class HtmlContentViewer extends StatefulWidget {
|
||||||
|
|
||||||
@@ -29,6 +30,7 @@ class HtmlContentViewer extends StatefulWidget {
|
|||||||
final OnMailtoDelegateAction? onMailtoDelegateAction;
|
final OnMailtoDelegateAction? onMailtoDelegateAction;
|
||||||
final OnScrollHorizontalEndAction? onScrollHorizontalEnd;
|
final OnScrollHorizontalEndAction? onScrollHorizontalEnd;
|
||||||
final OnPreviewEMLDelegateAction? onPreviewEMLDelegateAction;
|
final OnPreviewEMLDelegateAction? onPreviewEMLDelegateAction;
|
||||||
|
final OnDownloadAttachmentDelegateAction? onDownloadAttachmentDelegateAction;
|
||||||
|
|
||||||
const HtmlContentViewer({
|
const HtmlContentViewer({
|
||||||
Key? key,
|
Key? key,
|
||||||
@@ -39,6 +41,7 @@ class HtmlContentViewer extends StatefulWidget {
|
|||||||
this.onMailtoDelegateAction,
|
this.onMailtoDelegateAction,
|
||||||
this.onScrollHorizontalEnd,
|
this.onScrollHorizontalEnd,
|
||||||
this.onPreviewEMLDelegateAction,
|
this.onPreviewEMLDelegateAction,
|
||||||
|
this.onDownloadAttachmentDelegateAction,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -261,17 +264,23 @@ class _HtmlContentViewState extends State<HtmlContentViewer> {
|
|||||||
|
|
||||||
final requestUri = Uri.parse(url);
|
final requestUri = Uri.parse(url);
|
||||||
if (widget.onMailtoDelegateAction != null &&
|
if (widget.onMailtoDelegateAction != null &&
|
||||||
requestUri.isScheme('mailto')) {
|
requestUri.isScheme(Constant.mailtoScheme)) {
|
||||||
await widget.onMailtoDelegateAction?.call(requestUri);
|
await widget.onMailtoDelegateAction?.call(requestUri);
|
||||||
return NavigationActionPolicy.CANCEL;
|
return NavigationActionPolicy.CANCEL;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (widget.onPreviewEMLDelegateAction != null &&
|
if (widget.onPreviewEMLDelegateAction != null &&
|
||||||
requestUri.isScheme(PreviewEmlFileUtils.emlPreviewerScheme)) {
|
requestUri.isScheme(Constant.emlPreviewerScheme)) {
|
||||||
await widget.onPreviewEMLDelegateAction?.call(requestUri);
|
await widget.onPreviewEMLDelegateAction?.call(requestUri);
|
||||||
return NavigationActionPolicy.CANCEL;
|
return NavigationActionPolicy.CANCEL;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (widget.onDownloadAttachmentDelegateAction != null &&
|
||||||
|
requestUri.isScheme(Constant.attachmentScheme)) {
|
||||||
|
await widget.onDownloadAttachmentDelegateAction?.call(requestUri);
|
||||||
|
return NavigationActionPolicy.CANCEL;
|
||||||
|
}
|
||||||
|
|
||||||
if (await launcher.canLaunchUrl(Uri.parse(url))) {
|
if (await launcher.canLaunchUrl(Uri.parse(url))) {
|
||||||
await launcher.launchUrl(
|
await launcher.launchUrl(
|
||||||
Uri.parse(url),
|
Uri.parse(url),
|
||||||
|
|||||||
@@ -7,8 +7,6 @@ import 'package:html/dom.dart';
|
|||||||
import 'package:html/parser.dart';
|
import 'package:html/parser.dart';
|
||||||
|
|
||||||
class PreviewEmlFileUtils {
|
class PreviewEmlFileUtils {
|
||||||
static const String emlPreviewerScheme = 'eml-previewer';
|
|
||||||
|
|
||||||
Element? _createEmailElement({
|
Element? _createEmailElement({
|
||||||
required String subjectPrefix,
|
required String subjectPrefix,
|
||||||
required String fromPrefix,
|
required String fromPrefix,
|
||||||
|
|||||||
@@ -42,8 +42,6 @@ import 'package:tmail_ui_user/features/mailbox/domain/model/create_new_mailbox_r
|
|||||||
import 'package:tmail_ui_user/features/sending_queue/domain/model/sending_email.dart';
|
import 'package:tmail_ui_user/features/sending_queue/domain/model/sending_email.dart';
|
||||||
import 'package:tmail_ui_user/main/exceptions/exception_thrower.dart';
|
import 'package:tmail_ui_user/main/exceptions/exception_thrower.dart';
|
||||||
import 'package:tmail_ui_user/main/exceptions/send_email_exception_thrower.dart';
|
import 'package:tmail_ui_user/main/exceptions/send_email_exception_thrower.dart';
|
||||||
import 'package:tmail_ui_user/main/routes/app_routes.dart';
|
|
||||||
import 'package:tmail_ui_user/main/routes/route_utils.dart';
|
|
||||||
|
|
||||||
class EmailDataSourceImpl extends EmailDataSource {
|
class EmailDataSourceImpl extends EmailDataSource {
|
||||||
|
|
||||||
@@ -452,15 +450,11 @@ class EmailDataSourceImpl extends EmailDataSource {
|
|||||||
final iconBase64Data = await _fileUtils.convertImageAssetToBase64(
|
final iconBase64Data = await _fileUtils.convertImageAssetToBase64(
|
||||||
attachment.getIcon(_imagePaths));
|
attachment.getIcon(_imagePaths));
|
||||||
|
|
||||||
final link = attachment.isEMLFile
|
|
||||||
? _generateEMLAttachmentLink(attachment.blobId?.value)
|
|
||||||
: null;
|
|
||||||
|
|
||||||
final previewAttachment = PreviewAttachment(
|
final previewAttachment = PreviewAttachment(
|
||||||
iconBase64Data: iconBase64Data,
|
iconBase64Data: iconBase64Data,
|
||||||
name: attachment.name.escapeLtGtHtmlString(),
|
name: attachment.name.escapeLtGtHtmlString(),
|
||||||
size: filesize(attachment.size?.value),
|
size: filesize(attachment.size?.value),
|
||||||
link: link,
|
link: attachment.hyperLink,
|
||||||
);
|
);
|
||||||
|
|
||||||
listPreviewAttachment.add(previewAttachment);
|
listPreviewAttachment.add(previewAttachment);
|
||||||
@@ -498,21 +492,6 @@ class EmailDataSourceImpl extends EmailDataSource {
|
|||||||
}).catchError(_exceptionThrower.throwException);
|
}).catchError(_exceptionThrower.throwException);
|
||||||
}
|
}
|
||||||
|
|
||||||
String _generateEMLAttachmentLink(String? blobId) {
|
|
||||||
if (blobId == null) return '';
|
|
||||||
|
|
||||||
if (PlatformInfo.isWeb) {
|
|
||||||
return RouteUtils.createUrlWebLocationBar(
|
|
||||||
AppRoutes.emailEMLPreviewer,
|
|
||||||
previewId: blobId,
|
|
||||||
).toString();
|
|
||||||
} else if (PlatformInfo.isMobile) {
|
|
||||||
return '${PreviewEmlFileUtils.emlPreviewerScheme}://$blobId';
|
|
||||||
} else {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<String> _transformEmailContent(
|
Future<String> _transformEmailContent(
|
||||||
List<EmailContent> emailContents,
|
List<EmailContent> emailContents,
|
||||||
Map<String, String> mapCidImageDownloadUrl,
|
Map<String, String> mapCidImageDownloadUrl,
|
||||||
|
|||||||
@@ -2059,6 +2059,8 @@ class SingleEmailController extends BaseController with AppLoaderMixin {
|
|||||||
emlPreviewer: emlPreviewer,
|
emlPreviewer: emlPreviewer,
|
||||||
onMailtoDelegateAction: openMailToLink,
|
onMailtoDelegateAction: openMailToLink,
|
||||||
onPreviewEMLDelegateAction: (uri) => _openEMLPreviewer(context, uri),
|
onPreviewEMLDelegateAction: (uri) => _openEMLPreviewer(context, uri),
|
||||||
|
onDownloadAttachmentDelegateAction: (uri) =>
|
||||||
|
_downloadAttachmentInEMLPreview(context, uri),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -2069,13 +2071,23 @@ class SingleEmailController extends BaseController with AppLoaderMixin {
|
|||||||
log('SingleEmailController::_openEMLPreviewer:uri = $uri');
|
log('SingleEmailController::_openEMLPreviewer:uri = $uri');
|
||||||
if (uri == null) return;
|
if (uri == null) return;
|
||||||
|
|
||||||
final blobId = uri.authority;
|
final blobId = uri.path;
|
||||||
log('SingleEmailController::_openEMLPreviewer:blobId = $blobId');
|
log('SingleEmailController::_openEMLPreviewer:blobId = $blobId');
|
||||||
if (blobId.isEmpty) return;
|
if (blobId.isEmpty) return;
|
||||||
|
|
||||||
previewEMLFileAction(Id(blobId), AppLocalizations.of(context));
|
previewEMLFileAction(Id(blobId), AppLocalizations.of(context));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _downloadAttachmentInEMLPreview(BuildContext context, Uri? uri) async {
|
||||||
|
log('SingleEmailController::_downloadAttachmentInEMLPreview:uri = $uri');
|
||||||
|
if (uri == null) return;
|
||||||
|
|
||||||
|
final attachment = EmailUtils.parsingAttachmentByUri(uri);
|
||||||
|
if (attachment == null) return;
|
||||||
|
|
||||||
|
handleDownloadAttachmentAction(context, attachment);
|
||||||
|
}
|
||||||
|
|
||||||
void handleMailToAttendees(CalendarOrganizer? organizer, List<CalendarAttendee>? attendees) {
|
void handleMailToAttendees(CalendarOrganizer? organizer, List<CalendarAttendee>? attendees) {
|
||||||
final listEmailAddressAttendees = attendees
|
final listEmailAddressAttendees = attendees
|
||||||
?.map((attendee) => EmailAddress(attendee.name?.name, attendee.mailto?.mailAddress.value))
|
?.map((attendee) => EmailAddress(attendee.name?.name, attendee.mailto?.mailAddress.value))
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
|
import 'package:core/data/constants/constant.dart';
|
||||||
import 'package:core/presentation/extensions/media_type_extension.dart';
|
import 'package:core/presentation/extensions/media_type_extension.dart';
|
||||||
import 'package:core/presentation/resources/image_paths.dart';
|
import 'package:core/presentation/resources/image_paths.dart';
|
||||||
|
import 'package:core/utils/platform_info.dart';
|
||||||
import 'package:model/email/attachment.dart';
|
import 'package:model/email/attachment.dart';
|
||||||
|
import 'package:tmail_ui_user/main/routes/app_routes.dart';
|
||||||
|
import 'package:tmail_ui_user/main/routes/route_utils.dart';
|
||||||
|
|
||||||
extension AttachmentExtension on Attachment {
|
extension AttachmentExtension on Attachment {
|
||||||
String getIcon(ImagePaths imagePaths) => type?.getIcon(imagePaths, fileName: name) ?? imagePaths.icFileEPup;
|
String getIcon(ImagePaths imagePaths) => type?.getIcon(imagePaths, fileName: name) ?? imagePaths.icFileEPup;
|
||||||
@@ -8,4 +12,27 @@ extension AttachmentExtension on Attachment {
|
|||||||
bool get isPDFFile => type?.isPDFFile(fileName: name) ?? false;
|
bool get isPDFFile => type?.isPDFFile(fileName: name) ?? false;
|
||||||
|
|
||||||
bool get isEMLFile => type?.isEMLFile ?? false;
|
bool get isEMLFile => type?.isEMLFile ?? false;
|
||||||
|
|
||||||
|
String get hyperLink => isEMLFile ? emlLink : attachmentLink;
|
||||||
|
|
||||||
|
String get emlLink {
|
||||||
|
if (blobId == null) return '';
|
||||||
|
|
||||||
|
if (PlatformInfo.isWeb) {
|
||||||
|
return RouteUtils.createUrlWebLocationBar(
|
||||||
|
AppRoutes.emailEMLPreviewer,
|
||||||
|
previewId: blobId!.value,
|
||||||
|
).toString();
|
||||||
|
} else if (PlatformInfo.isMobile) {
|
||||||
|
return '${Constant.emlPreviewerScheme}:${blobId!.value}';
|
||||||
|
} else {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String get attachmentLink {
|
||||||
|
if (blobId == null) return '';
|
||||||
|
|
||||||
|
return '${Constant.attachmentScheme}:${blobId!.value}?name=${name ?? ''}&size=${size?.value ?? ''}&type=${type?.mimeType ?? ''}';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -5,11 +5,15 @@ import 'package:core/utils/app_logger.dart';
|
|||||||
import 'package:core/utils/mail/mail_address.dart';
|
import 'package:core/utils/mail/mail_address.dart';
|
||||||
import 'package:get/get_utils/src/get_utils/get_utils.dart';
|
import 'package:get/get_utils/src/get_utils/get_utils.dart';
|
||||||
import 'package:dartz/dartz.dart';
|
import 'package:dartz/dartz.dart';
|
||||||
|
import 'package:http_parser/http_parser.dart';
|
||||||
import 'package:jmap_dart_client/jmap/account_id.dart';
|
import 'package:jmap_dart_client/jmap/account_id.dart';
|
||||||
import 'package:jmap_dart_client/jmap/core/capability/capability_identifier.dart';
|
import 'package:jmap_dart_client/jmap/core/capability/capability_identifier.dart';
|
||||||
|
import 'package:jmap_dart_client/jmap/core/id.dart';
|
||||||
import 'package:jmap_dart_client/jmap/core/properties/properties.dart';
|
import 'package:jmap_dart_client/jmap/core/properties/properties.dart';
|
||||||
import 'package:jmap_dart_client/jmap/core/session/session.dart';
|
import 'package:jmap_dart_client/jmap/core/session/session.dart';
|
||||||
|
import 'package:jmap_dart_client/jmap/core/unsigned_int.dart';
|
||||||
import 'package:jmap_dart_client/jmap/mail/email/email_address.dart';
|
import 'package:jmap_dart_client/jmap/mail/email/email_address.dart';
|
||||||
|
import 'package:model/email/attachment.dart';
|
||||||
import 'package:tmail_ui_user/features/email/domain/state/download_attachment_for_web_state.dart';
|
import 'package:tmail_ui_user/features/email/domain/state/download_attachment_for_web_state.dart';
|
||||||
import 'package:tmail_ui_user/features/email/presentation/model/email_unsubscribe.dart';
|
import 'package:tmail_ui_user/features/email/presentation/model/email_unsubscribe.dart';
|
||||||
import 'package:tmail_ui_user/features/thread/domain/constants/thread_constants.dart';
|
import 'package:tmail_ui_user/features/thread/domain/constants/thread_constants.dart';
|
||||||
@@ -190,4 +194,24 @@ class EmailUtils {
|
|||||||
final mailtoLinks = extractMailtoLinksFromListPost(listPost);
|
final mailtoLinks = extractMailtoLinksFromListPost(listPost);
|
||||||
return extractRecipientsFromListMailtoLink(mailtoLinks);
|
return extractRecipientsFromListMailtoLink(mailtoLinks);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static Attachment? parsingAttachmentByUri(Uri uri) {
|
||||||
|
try {
|
||||||
|
final blobId = uri.path;
|
||||||
|
final queryParams = uri.queryParameters;
|
||||||
|
final name = queryParams['name'];
|
||||||
|
final size = queryParams['size'];
|
||||||
|
final type = queryParams['type'];
|
||||||
|
log('EmailUtils::parsingAttachmentByUri:blobId = $blobId | name = $name | size = $size | type = $type');
|
||||||
|
return Attachment(
|
||||||
|
blobId: Id(blobId),
|
||||||
|
name: name,
|
||||||
|
size: size?.isNotEmpty == true ? UnsignedInt(int.parse(size!)) : null,
|
||||||
|
type: type?.isNotEmpty == true ? MediaType.parse(type!) : null,
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
logError('EmailUtils::parsingAttachmentByUri:Exception = $e:');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -10,12 +10,14 @@ class EmailPreviewerDialogView extends StatelessWidget {
|
|||||||
final EMLPreviewer emlPreviewer;
|
final EMLPreviewer emlPreviewer;
|
||||||
final OnMailtoDelegateAction onMailtoDelegateAction;
|
final OnMailtoDelegateAction onMailtoDelegateAction;
|
||||||
final OnPreviewEMLDelegateAction onPreviewEMLDelegateAction;
|
final OnPreviewEMLDelegateAction onPreviewEMLDelegateAction;
|
||||||
|
final OnDownloadAttachmentDelegateAction onDownloadAttachmentDelegateAction;
|
||||||
|
|
||||||
const EmailPreviewerDialogView({
|
const EmailPreviewerDialogView({
|
||||||
super.key,
|
super.key,
|
||||||
required this.emlPreviewer,
|
required this.emlPreviewer,
|
||||||
required this.onMailtoDelegateAction,
|
required this.onMailtoDelegateAction,
|
||||||
required this.onPreviewEMLDelegateAction,
|
required this.onPreviewEMLDelegateAction,
|
||||||
|
required this.onDownloadAttachmentDelegateAction,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -29,6 +31,7 @@ class EmailPreviewerDialogView extends StatelessWidget {
|
|||||||
direction: AppUtils.getCurrentDirection(context),
|
direction: AppUtils.getCurrentDirection(context),
|
||||||
onMailtoDelegateAction: onMailtoDelegateAction,
|
onMailtoDelegateAction: onMailtoDelegateAction,
|
||||||
onPreviewEMLDelegateAction: onPreviewEMLDelegateAction,
|
onPreviewEMLDelegateAction: onPreviewEMLDelegateAction,
|
||||||
|
onDownloadAttachmentDelegateAction: onDownloadAttachmentDelegateAction,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,63 @@
|
|||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:tmail_ui_user/features/email/presentation/utils/email_utils.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
group('parsingAttachmentByUri', () {
|
||||||
|
test('should parse a valid URI and return an Attachment object', () {
|
||||||
|
final uri = Uri.parse(
|
||||||
|
'attachment:blobId?name=testfile.txt&size=1024&type=text/plain',
|
||||||
|
);
|
||||||
|
|
||||||
|
final attachment = EmailUtils.parsingAttachmentByUri(uri);
|
||||||
|
|
||||||
|
expect(attachment, isNotNull);
|
||||||
|
expect(attachment!.blobId?.value, 'blobId');
|
||||||
|
expect(attachment.name, 'testfile.txt');
|
||||||
|
expect(attachment.size?.value, 1024);
|
||||||
|
expect(attachment.type?.toString(), 'text/plain');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return null for an invalid URI', () {
|
||||||
|
final uri = Uri.parse('https://example.com');
|
||||||
|
|
||||||
|
final attachment = EmailUtils.parsingAttachmentByUri(uri);
|
||||||
|
|
||||||
|
expect(attachment, isNull);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should handle missing optional parameters gracefully', () {
|
||||||
|
final uri = Uri.parse('attachment:blobId?name=testfile.txt');
|
||||||
|
|
||||||
|
final attachment = EmailUtils.parsingAttachmentByUri(uri);
|
||||||
|
|
||||||
|
expect(attachment, isNotNull);
|
||||||
|
expect(attachment!.blobId?.value, 'blobId');
|
||||||
|
expect(attachment.name, 'testfile.txt');
|
||||||
|
expect(attachment.size, isNull);
|
||||||
|
expect(attachment.type, isNull);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should handle invalid size parameter gracefully', () {
|
||||||
|
final uri = Uri.parse(
|
||||||
|
'attachment:blobId?name=testfile.txt&size=invalid',
|
||||||
|
);
|
||||||
|
|
||||||
|
final attachment = EmailUtils.parsingAttachmentByUri(uri);
|
||||||
|
|
||||||
|
expect(attachment, isNull);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should handle empty type parameter gracefully', () {
|
||||||
|
final uri = Uri.parse(
|
||||||
|
'attachment:blobId?name=testfile.txt&type=',
|
||||||
|
);
|
||||||
|
|
||||||
|
final attachment = EmailUtils.parsingAttachmentByUri(uri);
|
||||||
|
|
||||||
|
expect(attachment, isNotNull);
|
||||||
|
expect(attachment!.blobId?.value, 'blobId');
|
||||||
|
expect(attachment.name, 'testfile.txt');
|
||||||
|
expect(attachment.type, isNull);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user