TF-2160 Fix email content is cut off

(cherry picked from commit 045cd382add2538858b24491b41bd597371239c8)
This commit is contained in:
dab246
2023-09-26 15:25:29 +07:00
committed by Dat H. Pham
parent c9d84a1981
commit be7eada7b7
17 changed files with 57 additions and 118 deletions
@@ -12,8 +12,8 @@ abstract class DomTransformer {
/// All changes will be visible to subsequent transformers. /// All changes will be visible to subsequent transformers.
Future<void> process({ Future<void> process({
required Document document, required Document document,
required DioClient dioClient,
Map<String, String>? mapUrlDownloadCID, Map<String, String>? mapUrlDownloadCID,
DioClient? dioClient,
}); });
/// Adds a HEAD element if necessary /// Adds a HEAD element if necessary
@@ -8,8 +8,8 @@ class AddTargetBlankInTagATransformer extends DomTransformer {
@override @override
Future<void> process({ Future<void> process({
required Document document, required Document document,
required DioClient dioClient,
Map<String, String>? mapUrlDownloadCID, Map<String, String>? mapUrlDownloadCID,
DioClient? dioClient,
}) async { }) async {
final elements = document.querySelectorAll('a'); final elements = document.querySelectorAll('a');
await Future.wait(elements.map((element) async { await Future.wait(elements.map((element) async {
@@ -11,8 +11,8 @@ class AddTooltipLinkTransformer extends DomTransformer {
@override @override
Future<void> process({ Future<void> process({
required Document document, required Document document,
required DioClient dioClient,
Map<String, String>? mapUrlDownloadCID, Map<String, String>? mapUrlDownloadCID,
DioClient? dioClient
}) async { }) async {
final linkElements = document.querySelectorAll('a[href^="http"]'); final linkElements = document.querySelectorAll('a[href^="http"]');
await Future.wait(linkElements.map((linkElement) async { await Future.wait(linkElements.map((linkElement) async {
@@ -10,8 +10,8 @@ class BlockCodeTransformer extends DomTransformer {
@override @override
Future<void> process({ Future<void> process({
required Document document, required Document document,
required DioClient dioClient,
Map<String, String>? mapUrlDownloadCID, Map<String, String>? mapUrlDownloadCID,
DioClient? dioClient
}) async { }) async {
final codeElements = document.getElementsByTagName('pre'); final codeElements = document.getElementsByTagName('pre');
await Future.wait(codeElements.map((element) async { await Future.wait(codeElements.map((element) async {
@@ -10,8 +10,8 @@ class BlockQuotedTransformer extends DomTransformer {
@override @override
Future<void> process({ Future<void> process({
required Document document, required Document document,
required DioClient dioClient,
Map<String, String>? mapUrlDownloadCID, Map<String, String>? mapUrlDownloadCID,
DioClient? dioClient
}) async { }) async {
final quotedElements = document.getElementsByTagName('blockquote'); final quotedElements = document.getElementsByTagName('blockquote');
await Future.wait(quotedElements.map((quotedElement) async { await Future.wait(quotedElements.map((quotedElement) async {
@@ -12,29 +12,33 @@ import 'package:html/dom.dart';
class ImageTransformer extends DomTransformer { class ImageTransformer extends DomTransformer {
final bool useLoadingAttribute; const ImageTransformer();
const ImageTransformer({this.useLoadingAttribute = false});
@override @override
Future<void> process({ Future<void> process({
required Document document, required Document document,
required DioClient dioClient,
Map<String, String>? mapUrlDownloadCID, Map<String, String>? mapUrlDownloadCID,
DioClient? dioClient
}) async { }) async {
final imageElements = document.querySelectorAll('img'); final imageElements = document.querySelectorAll('img');
await Future.wait(imageElements.map((imageElement) async { await Future.wait(imageElements.map((imageElement) async {
final exStyle = imageElement.attributes['style']; var exStyle = imageElement.attributes['style'];
if (exStyle != null) { if (exStyle != null) {
imageElement.attributes['style'] = '$exStyle display: inline;max-width: 100%;'; if (!exStyle.contains('display')) {
exStyle = '$exStyle display:inline;';
}
if (!exStyle.contains('max-width')) {
exStyle = '$exStyle max-width:100%;';
}
imageElement.attributes['style'] = exStyle;
} else { } else {
imageElement.attributes['style'] = 'display: inline;max-width: 100%;'; imageElement.attributes['style'] = 'display:inline;max-width:100%;';
} }
final src = imageElement.attributes['src']; final src = imageElement.attributes['src'];
if (src == null) return; if (src == null) return;
if (src.startsWith('cid:') && dioClient != null && mapUrlDownloadCID != null) { if (src.startsWith('cid:') && mapUrlDownloadCID != null) {
final imageBase64 = await _convertCidToBase64Image( final imageBase64 = await _convertCidToBase64Image(
dioClient: dioClient, dioClient: dioClient,
mapUrlDownloadCID: mapUrlDownloadCID, mapUrlDownloadCID: mapUrlDownloadCID,
@@ -42,18 +46,8 @@ class ImageTransformer extends DomTransformer {
); );
imageElement.attributes['src'] = imageBase64 ?? src; imageElement.attributes['src'] = imageBase64 ?? src;
} else if (src.startsWith('https://') || src.startsWith('http://')) { } else if (src.startsWith('https://') || src.startsWith('http://')) {
if (useLoadingAttribute) { if (!imageElement.attributes.containsKey('loading')) {
imageElement.attributes['loading'] = 'lazy'; imageElement.attributes['loading'] = 'lazy';
} else {
final classAttribute = imageElement.attributes['class'];
if (classAttribute != null) {
imageElement.attributes['class'] = '$classAttribute lazy-loading';
} else {
imageElement.attributes['class'] = 'lazy-loading';
}
imageElement.attributes['data-src'] = src;
imageElement.attributes.remove('src');
imageElement.attributes.remove('loading');
} }
} }
})); }));
@@ -12,8 +12,8 @@ class RemoveTooltipLinkTransformer extends DomTransformer {
@override @override
Future<void> process({ Future<void> process({
required Document document, required Document document,
required DioClient dioClient,
Map<String, String>? mapUrlDownloadCID, Map<String, String>? mapUrlDownloadCID,
DioClient? dioClient
}) async { }) async {
final linkElements = document.querySelectorAll('a.$nameClassToolTip'); final linkElements = document.querySelectorAll('a.$nameClassToolTip');
await Future.wait(linkElements.map((linkElement) async { await Future.wait(linkElements.map((linkElement) async {
@@ -1,32 +0,0 @@
import 'package:core/data/network/dio_client.dart';
import 'package:core/presentation/utils/html_transformer/base/dom_transformer.dart';
import 'package:html/dom.dart';
class ReplaceLazyLoadImageTransformer extends DomTransformer {
const ReplaceLazyLoadImageTransformer();
@override
Future<void> process({
required Document document,
Map<String, String>? mapUrlDownloadCID,
DioClient? dioClient
}) async {
final imageElements = document.querySelectorAll('img.lazy-loading');
await Future.wait(imageElements.map((imageElement) async {
final classAttribute = imageElement.attributes['class'];
if (classAttribute != null) {
final newClassAttribute = classAttribute.replaceFirst('lazy-loading', '');
imageElement.attributes['class'] = newClassAttribute;
}
final dataSrc = imageElement.attributes['data-src'];
if (dataSrc != null) {
imageElement.attributes['src'] = dataSrc;
imageElement.attributes.remove('data-src');
}
imageElement.attributes['loading'] = 'lazy';
}));
}
}
@@ -10,8 +10,8 @@ class RemoveScriptTransformer extends DomTransformer {
@override @override
Future<void> process({ Future<void> process({
required Document document, required Document document,
required DioClient dioClient,
Map<String, String>? mapUrlDownloadCID, Map<String, String>? mapUrlDownloadCID,
DioClient? dioClient
}) async { }) async {
final scriptElements = document.getElementsByTagName('script'); final scriptElements = document.getElementsByTagName('script');
await Future.wait(scriptElements.map((scriptElement) async { await Future.wait(scriptElements.map((scriptElement) async {
@@ -10,8 +10,8 @@ class SignatureTransformer extends DomTransformer {
@override @override
Future<void> process({ Future<void> process({
required Document document, required Document document,
required DioClient dioClient,
Map<String, String>? mapUrlDownloadCID, Map<String, String>? mapUrlDownloadCID,
DioClient? dioClient
}) async { }) async {
final signatureElements = document.querySelectorAll('div.tmail-signature'); final signatureElements = document.querySelectorAll('div.tmail-signature');
await Future.wait(signatureElements.map((element) async { await Future.wait(signatureElements.map((element) async {
@@ -79,6 +79,7 @@ class HtmlUtils {
// Replace the placeholder with the actual image source // Replace the placeholder with the actual image source
img.src = src; img.src = src;
img.removeAttribute("data-src");
// Stop observing the image // Stop observing the image
observer.unobserve(img); observer.unobserve(img);
@@ -27,8 +27,8 @@ class MessageContentTransformer {
..._configuration.domTransformers.map((domTransformer) async => ..._configuration.domTransformers.map((domTransformer) async =>
domTransformer.process( domTransformer.process(
document: document, document: document,
dioClient: _dioClient,
mapUrlDownloadCID: mapUrlDownloadCID, mapUrlDownloadCID: mapUrlDownloadCID,
dioClient: _dioClient
) )
) )
]); ]);
@@ -7,7 +7,6 @@ import 'package:core/presentation/utils/html_transformer/dom/blockcode_transform
import 'package:core/presentation/utils/html_transformer/dom/blockquoted_transformers.dart'; import 'package:core/presentation/utils/html_transformer/dom/blockquoted_transformers.dart';
import 'package:core/presentation/utils/html_transformer/dom/image_transformers.dart'; import 'package:core/presentation/utils/html_transformer/dom/image_transformers.dart';
import 'package:core/presentation/utils/html_transformer/dom/remove_tooltip_link_transformers.dart'; import 'package:core/presentation/utils/html_transformer/dom/remove_tooltip_link_transformers.dart';
import 'package:core/presentation/utils/html_transformer/dom/replace_lazy_load_image_transformer.dart';
import 'package:core/presentation/utils/html_transformer/dom/script_transformers.dart'; import 'package:core/presentation/utils/html_transformer/dom/script_transformers.dart';
import 'package:core/presentation/utils/html_transformer/dom/sigature_transformers.dart'; import 'package:core/presentation/utils/html_transformer/dom/sigature_transformers.dart';
import 'package:core/presentation/utils/html_transformer/text/sanitize_autolink_html_transformers.dart'; import 'package:core/presentation/utils/html_transformer/text/sanitize_autolink_html_transformers.dart';
@@ -29,51 +28,19 @@ class TransformConfiguration {
this.textTransformers this.textTransformers
); );
factory TransformConfiguration.forReplyForwardEmail() => TransformConfiguration.create( factory TransformConfiguration.fromDomTransformers(List<DomTransformer> domTransformers) => TransformConfiguration(domTransformers, []);
customDomTransformers: [
const ReplaceLazyLoadImageTransformer(),
if (PlatformInfo.isWeb)
const RemoveTooltipLinkTransformer(),
const SignatureTransformer(),
]
);
factory TransformConfiguration.forDraftsEmail() => TransformConfiguration.create( factory TransformConfiguration.empty() => const TransformConfiguration([], []);
customDomTransformers: [
const RemoveScriptTransformer(),
const BlockQuotedTransformer(),
const BlockCodeTransformer(),
const AddTargetBlankInTagATransformer(),
const ImageTransformer(useLoadingAttribute: true),
]
);
factory TransformConfiguration.forComposeEmailPlatformWeb() => TransformConfiguration.create( factory TransformConfiguration.forReplyForwardEmail() => TransformConfiguration.fromDomTransformers([
customDomTransformers: [ if (PlatformInfo.isWeb)
const RemoveScriptTransformer(), const RemoveTooltipLinkTransformer(),
const BlockQuotedTransformer(), const SignatureTransformer(),
const BlockCodeTransformer(), ]);
const AddTargetBlankInTagATransformer(),
const ImageTransformer(useLoadingAttribute: true),
const SignatureTransformer(),
]
);
factory TransformConfiguration.forComposeEmail() => TransformConfiguration.create( factory TransformConfiguration.forDraftsEmail() => TransformConfiguration.empty();
customDomTransformers: [
const RemoveScriptTransformer(),
const BlockQuotedTransformer(),
const BlockCodeTransformer(),
const AddTargetBlankInTagATransformer(),
const ImageTransformer(),
const SignatureTransformer(),
],
customTextTransformers: [
const SanitizeAutolinkHtmlTransformers()
]
);
factory TransformConfiguration.forPreviewEmailPlatformWeb() => TransformConfiguration.create( factory TransformConfiguration.forPreviewEmailOnWeb() => TransformConfiguration.create(
customDomTransformers: [ customDomTransformers: [
const RemoveScriptTransformer(), const RemoveScriptTransformer(),
const BlockQuotedTransformer(), const BlockQuotedTransformer(),
@@ -84,6 +51,8 @@ class TransformConfiguration {
] ]
); );
factory TransformConfiguration.forPreviewEmail() => TransformConfiguration.standardConfiguration;
/// Provides easy access to a standard configuration that does not block external images. /// Provides easy access to a standard configuration that does not block external images.
static const TransformConfiguration standardConfiguration = TransformConfiguration( static const TransformConfiguration standardConfiguration = TransformConfiguration(
standardDomTransformers, standardDomTransformers,
@@ -1,5 +1,6 @@
import 'dart:async'; import 'dart:async';
import 'dart:io'; import 'dart:io';
import 'dart:math';
import 'package:core/presentation/extensions/color_extension.dart'; import 'package:core/presentation/extensions/color_extension.dart';
import 'package:core/presentation/utils/html_transformer/html_event_action.dart'; import 'package:core/presentation/utils/html_transformer/html_event_action.dart';
@@ -54,7 +55,6 @@ class HtmlContentViewer extends StatefulWidget {
class _HtmlContentViewState extends State<HtmlContentViewer> { class _HtmlContentViewState extends State<HtmlContentViewer> {
late double actualHeight; late double actualHeight;
double minHeight = 100;
double minWidth = 300; double minWidth = 300;
String? _htmlData; String? _htmlData;
late InAppWebViewController _webViewController; late InAppWebViewController _webViewController;
@@ -108,6 +108,7 @@ class _HtmlContentViewState extends State<HtmlContentViewer> {
widget.onCreated?.call(controller); widget.onCreated?.call(controller);
}, },
onLoadStop: _onLoadStop, onLoadStop: _onLoadStop,
onContentSizeChanged: _onContentSizeChanged,
shouldOverrideUrlLoading: _shouldOverrideUrlLoading, shouldOverrideUrlLoading: _shouldOverrideUrlLoading,
gestureRecognizers: { gestureRecognizers: {
Factory<LongPressGestureRecognizer>(() => LongPressGestureRecognizer()), Factory<LongPressGestureRecognizer>(() => LongPressGestureRecognizer()),
@@ -152,6 +153,21 @@ class _HtmlContentViewState extends State<HtmlContentViewer> {
); );
} }
void _onContentSizeChanged(
InAppWebViewController controller,
Size oldContentSize,
Size newContentSize
) async {
log('_HtmlContentViewState::_onContentSizeChanged:oldContentSize: $oldContentSize | newContentSize: $newContentSize');
final maxContentHeight = max(oldContentSize.height, newContentSize.height);
log('_HtmlContentViewState::_onContentSizeChanged:maxContentHeight: $maxContentHeight');
if (maxContentHeight > actualHeight) {
setState(() {
actualHeight = maxContentHeight;
});
}
}
void _onHandleScrollEvent(List<dynamic> parameters) { void _onHandleScrollEvent(List<dynamic> parameters) {
log('_HtmlContentViewState::_onHandleScrollRightEvent():parameters: $parameters'); log('_HtmlContentViewState::_onHandleScrollRightEvent():parameters: $parameters');
final message = parameters.first; final message = parameters.first;
@@ -167,13 +183,11 @@ class _HtmlContentViewState extends State<HtmlContentViewer> {
final scrollHeight = await _webViewController.evaluateJavascript(source: 'document.body.scrollHeight'); final scrollHeight = await _webViewController.evaluateJavascript(source: 'document.body.scrollHeight');
if (scrollHeight != null && mounted) { if (scrollHeight != null && mounted) {
final scrollHeightWithBuffer = scrollHeight + 30.0; final scrollHeightWithBuffer = scrollHeight + 30.0;
if (scrollHeightWithBuffer > minHeight) { if (scrollHeightWithBuffer > actualHeight) {
setState(() { setState(() {
actualHeight = scrollHeightWithBuffer; actualHeight = scrollHeightWithBuffer;
_isLoading = false; _isLoading = false;
}); });
} else {
actualHeight = minHeight;
} }
} }
} }
@@ -1297,19 +1297,12 @@ class ComposerController extends BaseController {
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) {
TransformConfiguration transformConfiguration = TransformConfiguration.forComposeEmail();
if (isDraftEmail) {
transformConfiguration = TransformConfiguration.forDraftsEmail();
} else if (PlatformInfo.isWeb) {
transformConfiguration = TransformConfiguration.forComposeEmailPlatformWeb();
}
consumeState(_getEmailContentInteractor.execute( consumeState(_getEmailContentInteractor.execute(
session, session,
accountId, accountId,
emailId, emailId,
mailboxDashBoardController.baseDownloadUrl, mailboxDashBoardController.baseDownloadUrl,
transformConfiguration TransformConfiguration.empty()
)); ));
} }
} }
@@ -69,11 +69,11 @@ class GetListDetailedEmailByIdInteractor {
accountId: accountId, accountId: accountId,
downloadUrl: baseDownloadUrl downloadUrl: baseDownloadUrl
); );
TransformConfiguration transformConfiguration = TransformConfiguration.standardConfiguration; TransformConfiguration transformConfiguration = TransformConfiguration.forPreviewEmail();
if (email.isDraft) { if (email.isDraft) {
transformConfiguration = TransformConfiguration.forDraftsEmail(); transformConfiguration = TransformConfiguration.forDraftsEmail();
} else if (PlatformInfo.isWeb) { } else if (PlatformInfo.isWeb) {
transformConfiguration = TransformConfiguration.forPreviewEmailPlatformWeb(); transformConfiguration = TransformConfiguration.forPreviewEmailOnWeb();
} }
final newEmailContents = await _emailRepository.transformEmailContent( final newEmailContents = await _emailRepository.transformEmailContent(
email.emailContentList, email.emailContentList,
@@ -393,8 +393,8 @@ class SingleEmailController extends BaseController with AppLoaderMixin {
if (session != null && accountId != null) { if (session != null && accountId != null) {
final baseDownloadUrl = mailboxDashBoardController.sessionCurrent?.getDownloadUrl(jmapUrl: _dynamicUrlInterceptors.jmapUrl) ?? ''; final baseDownloadUrl = mailboxDashBoardController.sessionCurrent?.getDownloadUrl(jmapUrl: _dynamicUrlInterceptors.jmapUrl) ?? '';
TransformConfiguration transformConfiguration = PlatformInfo.isWeb TransformConfiguration transformConfiguration = PlatformInfo.isWeb
? TransformConfiguration.forPreviewEmailPlatformWeb() ? TransformConfiguration.forPreviewEmailOnWeb()
: TransformConfiguration.standardConfiguration; : TransformConfiguration.forPreviewEmail();
consumeState(_getEmailContentInteractor.execute( consumeState(_getEmailContentInteractor.execute(
session, session,