Fix displaying the external url of the link tag
This commit is contained in:
@@ -20,6 +20,10 @@ export 'presentation/utils/responsive_utils.dart';
|
|||||||
export 'presentation/utils/keyboard_utils.dart';
|
export 'presentation/utils/keyboard_utils.dart';
|
||||||
export 'presentation/utils/style_utils.dart';
|
export 'presentation/utils/style_utils.dart';
|
||||||
export 'presentation/utils/app_toast.dart';
|
export 'presentation/utils/app_toast.dart';
|
||||||
|
export 'presentation/utils/html_transformer/html_template.dart';
|
||||||
|
export 'presentation/utils/html_transformer/html_transform.dart';
|
||||||
|
export 'presentation/utils/html_transformer/transform_configuration.dart';
|
||||||
|
export 'presentation/utils/html_transformer/dom/add_tooltip_link_transformers.dart';
|
||||||
export 'data/utils/device_manager.dart';
|
export 'data/utils/device_manager.dart';
|
||||||
export 'utils/app_logger.dart';
|
export 'utils/app_logger.dart';
|
||||||
|
|
||||||
|
|||||||
@@ -13,8 +13,10 @@ abstract class DomTransformer {
|
|||||||
Future<void> process(
|
Future<void> process(
|
||||||
Document document,
|
Document document,
|
||||||
String message,
|
String message,
|
||||||
Map<String, String>? mapUrlDownloadCID,
|
{
|
||||||
DioClient dioClient
|
Map<String, String>? mapUrlDownloadCID,
|
||||||
|
DioClient? dioClient,
|
||||||
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
/// Adds a HEAD element if necessary
|
/// Adds a HEAD element if necessary
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
|
|
||||||
import 'package:core/presentation/utils/html_transformer/transform_configuration.dart';
|
|
||||||
|
|
||||||
/// Transforms plain text messages.
|
/// Transforms plain text messages.
|
||||||
abstract class TextTransformer {
|
abstract class TextTransformer {
|
||||||
const TextTransformer();
|
const TextTransformer();
|
||||||
|
|
||||||
String transform(String text, String message, TransformConfiguration configuration);
|
String process(String text);
|
||||||
}
|
}
|
||||||
+9
-13
@@ -1,40 +1,36 @@
|
|||||||
|
|
||||||
import 'package:core/data/network/dio_client.dart';
|
import 'package:core/data/network/dio_client.dart';
|
||||||
import 'package:core/presentation/utils/html_transformer/base/dom_transformer.dart';
|
import 'package:core/presentation/utils/html_transformer/base/dom_transformer.dart';
|
||||||
import 'package:core/utils/app_logger.dart';
|
|
||||||
import 'package:html/dom.dart';
|
import 'package:html/dom.dart';
|
||||||
|
|
||||||
class LinkTransformer extends DomTransformer {
|
class AddTooltipLinkTransformer extends DomTransformer {
|
||||||
|
|
||||||
const LinkTransformer();
|
const AddTooltipLinkTransformer();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> process(
|
Future<void> process(
|
||||||
Document document,
|
Document document,
|
||||||
String message,
|
String message,
|
||||||
Map<String, String>? mapUrlDownloadCID,
|
{
|
||||||
DioClient dioClient
|
Map<String, String>? mapUrlDownloadCID,
|
||||||
|
DioClient? dioClient
|
||||||
|
}
|
||||||
) async {
|
) async {
|
||||||
final linkElements = document.getElementsByTagName('a');
|
final linkElements = document.querySelectorAll('a[href^="http"]');
|
||||||
await Future.wait(linkElements.map((linkElement) async {
|
await Future.wait(linkElements.map((linkElement) async {
|
||||||
linkElement.attributes['rel'] = 'noopener noreferrer';
|
|
||||||
_addToolTipWhenHoverLink(linkElement);
|
_addToolTipWhenHoverLink(linkElement);
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
void _addToolTipWhenHoverLink(Element element) {
|
void _addToolTipWhenHoverLink(Element element) {
|
||||||
log('LinkTransformer::_addToolTipWhenHoverLink(): Before: ${element.outerHtml}');
|
|
||||||
final url = element.attributes['href'];
|
final url = element.attributes['href'];
|
||||||
final text = element.text;
|
final text = element.text;
|
||||||
final children = element.children;
|
final children = element.children;
|
||||||
if (children.isEmpty && text.isNotEmpty && url?.isNotEmpty == true) {
|
if (children.isEmpty && text.isNotEmpty && url != null) {
|
||||||
final innerHtml = element.innerHtml;
|
final innerHtml = element.innerHtml;
|
||||||
final tagClass = element.attributes['class'];
|
final tagClass = element.attributes['class'];
|
||||||
element.attributes['class'] = '$tagClass tooltip';
|
element.attributes['class'] = '$tagClass tooltip';
|
||||||
if (text.isNotEmpty && url != null && url.isNotEmpty) {
|
element.innerHtml = innerHtml + textHasToolTip(url);
|
||||||
element.innerHtml = innerHtml + textHasToolTip(url);
|
|
||||||
}
|
|
||||||
log('LinkTransformer::_addToolTipWhenHoverLink(): After: ${element.outerHtml}');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -11,8 +11,10 @@ class BlockQuotedTransformer extends DomTransformer {
|
|||||||
Future<void> process(
|
Future<void> process(
|
||||||
Document document,
|
Document document,
|
||||||
String message,
|
String message,
|
||||||
Map<String, String>? mapUrlDownloadCID,
|
{
|
||||||
DioClient dioClient
|
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 {
|
||||||
|
|||||||
@@ -14,8 +14,10 @@ class ImageTransformer extends DomTransformer {
|
|||||||
Future<void> process(
|
Future<void> process(
|
||||||
Document document,
|
Document document,
|
||||||
String message,
|
String message,
|
||||||
Map<String, String>? mapUrlDownloadCID,
|
{
|
||||||
DioClient dioClient
|
Map<String, String>? mapUrlDownloadCID,
|
||||||
|
DioClient? dioClient
|
||||||
|
}
|
||||||
) async {
|
) async {
|
||||||
final imageElements = document.getElementsByTagName('img');
|
final imageElements = document.getElementsByTagName('img');
|
||||||
|
|
||||||
@@ -29,7 +31,7 @@ class ImageTransformer extends DomTransformer {
|
|||||||
) {
|
) {
|
||||||
final cid = src.replaceFirst('cid:', '').trim();
|
final cid = src.replaceFirst('cid:', '').trim();
|
||||||
final cidUrlDownload = mapUrlDownloadCID[cid];
|
final cidUrlDownload = mapUrlDownloadCID[cid];
|
||||||
if (cidUrlDownload != null && cidUrlDownload.isNotEmpty) {
|
if (cidUrlDownload != null && cidUrlDownload.isNotEmpty && dioClient != null) {
|
||||||
final imgBase64 = await loadAsyncNetworkImageToBase64(dioClient, cidUrlDownload);
|
final imgBase64 = await loadAsyncNetworkImageToBase64(dioClient, cidUrlDownload);
|
||||||
if (imgBase64 != null && imgBase64.isNotEmpty) {
|
if (imgBase64 != null && imgBase64.isNotEmpty) {
|
||||||
imageElement.attributes['src'] = 'data:image/jpeg;base64,$imgBase64';
|
imageElement.attributes['src'] = 'data:image/jpeg;base64,$imgBase64';
|
||||||
|
|||||||
@@ -1,50 +0,0 @@
|
|||||||
|
|
||||||
import 'package:core/data/network/dio_client.dart';
|
|
||||||
import 'package:html/dom.dart';
|
|
||||||
import 'package:core/presentation/utils/html_transformer/base/dom_transformer.dart';
|
|
||||||
|
|
||||||
class MetaTransformer extends DomTransformer {
|
|
||||||
|
|
||||||
static final Element _viewPortMetaElement = Element.html(
|
|
||||||
'<meta name="viewport" content="width=device-width, initial-scale=1.0">');
|
|
||||||
static final Element _contentTypeMetaElement = Element.html(
|
|
||||||
'<meta http-equiv="Content-Type" content="text/html; charset=utf-8">');
|
|
||||||
|
|
||||||
const MetaTransformer();
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<void> process(
|
|
||||||
Document document,
|
|
||||||
String message,
|
|
||||||
Map<String, String>? mapUrlDownloadCID,
|
|
||||||
DioClient dioClient
|
|
||||||
) async {
|
|
||||||
final metaElements = document.getElementsByTagName('meta');
|
|
||||||
var viewportNeedsToBeAdded = true;
|
|
||||||
var contentTypeNeedsToBeAdded = true;
|
|
||||||
|
|
||||||
await Future.wait(metaElements.map((metaElement) async {
|
|
||||||
if (metaElement.attributes['name'] == 'viewport') {
|
|
||||||
viewportNeedsToBeAdded = false;
|
|
||||||
metaElement.attributes['content'] = 'width=device-width, initial-scale=1.0';
|
|
||||||
} else if (metaElement.attributes['charset'] != null) {
|
|
||||||
metaElement.attributes['charset'] = 'utf-8';
|
|
||||||
} else {
|
|
||||||
final httpEquiv = metaElement.attributes['http-equiv'];
|
|
||||||
if (httpEquiv != null && httpEquiv.toLowerCase() == 'content-type') {
|
|
||||||
contentTypeNeedsToBeAdded = false;
|
|
||||||
metaElement.attributes['content'] = 'text/html; charset=utf-8';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
|
|
||||||
if (contentTypeNeedsToBeAdded) {
|
|
||||||
ensureDocumentHeadIsAvailable(document);
|
|
||||||
document.head!.append(_contentTypeMetaElement);
|
|
||||||
}
|
|
||||||
if (viewportNeedsToBeAdded) {
|
|
||||||
ensureDocumentHeadIsAvailable(document);
|
|
||||||
document.head!.append(_viewPortMetaElement);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -11,8 +11,10 @@ class RemoveScriptTransformer extends DomTransformer {
|
|||||||
Future<void> process(
|
Future<void> process(
|
||||||
Document document,
|
Document document,
|
||||||
String message,
|
String message,
|
||||||
Map<String, String>? mapUrlDownloadCID,
|
{
|
||||||
DioClient dioClient
|
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 {
|
||||||
|
|||||||
@@ -0,0 +1,64 @@
|
|||||||
|
|
||||||
|
final tooltipLinkCss = '''
|
||||||
|
.tooltip .tooltiptext {
|
||||||
|
visibility: hidden;
|
||||||
|
max-width: 400px;
|
||||||
|
background-color: black;
|
||||||
|
color: #fff;
|
||||||
|
text-align: center;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 5px 8px 5px 8px;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
position: absolute;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
.tooltip:hover .tooltiptext {
|
||||||
|
visibility: visible;
|
||||||
|
}
|
||||||
|
''';
|
||||||
|
|
||||||
|
String generateHtml(String content, {
|
||||||
|
double? minHeight,
|
||||||
|
double? minWidth,
|
||||||
|
String? styleCSS,
|
||||||
|
String? javaScripts
|
||||||
|
}) {
|
||||||
|
return '''
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
|
||||||
|
<style>
|
||||||
|
.tmail-content {
|
||||||
|
min-height: ${minHeight ?? 0}px;
|
||||||
|
min-width: ${minWidth ?? 0}px;
|
||||||
|
color: #000000;
|
||||||
|
font-family: Inter;
|
||||||
|
font-size: 16px;
|
||||||
|
font-style: normal;
|
||||||
|
}
|
||||||
|
table {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
td {
|
||||||
|
padding: 13px;
|
||||||
|
margin: 0px;
|
||||||
|
}
|
||||||
|
th {
|
||||||
|
padding: 13px;
|
||||||
|
margin: 0px;
|
||||||
|
}
|
||||||
|
${styleCSS ?? ''}
|
||||||
|
</style>
|
||||||
|
${javaScripts ?? ''}
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="tmail-content">${content}</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
''';
|
||||||
|
}
|
||||||
@@ -7,17 +7,17 @@ class HtmlTransform {
|
|||||||
|
|
||||||
final String _contentHtml;
|
final String _contentHtml;
|
||||||
Map<String, String>? mapUrlDownloadCID;
|
Map<String, String>? mapUrlDownloadCID;
|
||||||
DioClient dioClient;
|
DioClient? dioClient;
|
||||||
|
|
||||||
HtmlTransform(
|
HtmlTransform(
|
||||||
this._contentHtml,
|
this._contentHtml,
|
||||||
this.dioClient,
|
{
|
||||||
this.mapUrlDownloadCID
|
this.mapUrlDownloadCID,
|
||||||
|
this.dioClient,
|
||||||
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
/// Transforms this message to HTML code.
|
/// Transforms this message to HTML code.
|
||||||
/// Optionally specify the [transformConfiguration] to control all aspects of the transformation
|
|
||||||
/// - in that case other parameters are ignored.
|
|
||||||
Future<String> transformToHtml({TransformConfiguration? transformConfiguration}) async {
|
Future<String> transformToHtml({TransformConfiguration? transformConfiguration}) async {
|
||||||
final document = await transformToDocument(transformConfiguration: transformConfiguration);
|
final document = await transformToDocument(transformConfiguration: transformConfiguration);
|
||||||
return document.outerHtml;
|
return document.outerHtml;
|
||||||
@@ -26,7 +26,7 @@ class HtmlTransform {
|
|||||||
/// Transforms this message to Document.
|
/// Transforms this message to Document.
|
||||||
Future<Document> transformToDocument({TransformConfiguration? transformConfiguration}) async {
|
Future<Document> transformToDocument({TransformConfiguration? transformConfiguration}) async {
|
||||||
transformConfiguration ??= TransformConfiguration.create();
|
transformConfiguration ??= TransformConfiguration.create();
|
||||||
final transformer = MessageContentTransformer(transformConfiguration, dioClient);
|
final transformer = MessageContentTransformer(transformConfiguration);
|
||||||
return await transformer.toDocument(_contentHtml, mapUrlDownloadCID);
|
return await transformer.toDocument(_contentHtml, mapUrlDownloadCID: mapUrlDownloadCID, dioClient: dioClient);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -7,24 +7,29 @@ import 'package:html/parser.dart' show parse;
|
|||||||
class MessageContentTransformer {
|
class MessageContentTransformer {
|
||||||
/// The configuration used for the transformation
|
/// The configuration used for the transformation
|
||||||
final TransformConfiguration configuration;
|
final TransformConfiguration configuration;
|
||||||
final DioClient dioClient;
|
|
||||||
|
|
||||||
MessageContentTransformer(this.configuration, this.dioClient);
|
MessageContentTransformer(this.configuration);
|
||||||
|
|
||||||
Future<void> transformDocument(
|
Future<void> transformDocument(
|
||||||
Document document,
|
Document document,
|
||||||
String message,
|
String message,
|
||||||
Map<String, String>? mapUrlDownloadCID,
|
{
|
||||||
|
Map<String, String>? mapUrlDownloadCID,
|
||||||
|
DioClient? dioClient
|
||||||
|
}
|
||||||
) async {
|
) async {
|
||||||
await Future.wait(configuration.domTransformers.map((domTransformer) async {
|
await Future.wait([
|
||||||
await domTransformer.process(document, message, mapUrlDownloadCID, dioClient);
|
if (configuration.domTransformers.isNotEmpty) ...configuration.domTransformers.map((domTransformer) async =>
|
||||||
}));
|
domTransformer.process(document, message, mapUrlDownloadCID: mapUrlDownloadCID, dioClient: dioClient)),
|
||||||
|
if (configuration.textTransformers.isNotEmpty) ...configuration.textTransformers.map((textTransformer) async =>
|
||||||
|
textTransformer.process(message))
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Document> toDocument(String message, Map<String, String>? mapUrlDownloadCID) async {
|
Future<Document> toDocument(String message, {Map<String, String>? mapUrlDownloadCID, DioClient? dioClient}) async {
|
||||||
var html = message;
|
var html = message;
|
||||||
final document = parse(html);
|
final document = parse(html);
|
||||||
await transformDocument(document, message, mapUrlDownloadCID);
|
await transformDocument(document, message, mapUrlDownloadCID: mapUrlDownloadCID, dioClient: dioClient);
|
||||||
return document;
|
return document;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
|
|
||||||
import 'package:core/presentation/utils/html_transformer/base/text_transformer.dart';
|
|
||||||
import 'package:core/presentation/utils/html_transformer/transform_configuration.dart';
|
|
||||||
|
|
||||||
class ConvertTagsTextTransformer implements TextTransformer {
|
|
||||||
|
|
||||||
const ConvertTagsTextTransformer();
|
|
||||||
|
|
||||||
@override
|
|
||||||
String transform(String text, String message, TransformConfiguration configuration) {
|
|
||||||
return text.replaceAll('<', '<').replaceAll('>', '>');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
|
|
||||||
import 'package:core/presentation/utils/html_transformer/base/text_transformer.dart';
|
|
||||||
import 'package:core/presentation/utils/html_transformer/transform_configuration.dart';
|
|
||||||
|
|
||||||
class LineBreakTextTransformer extends TextTransformer {
|
|
||||||
|
|
||||||
const LineBreakTextTransformer();
|
|
||||||
|
|
||||||
@override
|
|
||||||
String transform(String text, String message, TransformConfiguration configuration) {
|
|
||||||
text = text.replaceAll('\r\n', '<br/>');
|
|
||||||
text = text.replaceAll('\n', '<br/>');
|
|
||||||
return text;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
|
|
||||||
import 'package:core/presentation/utils/html_transformer/base/text_transformer.dart';
|
|
||||||
import 'package:core/presentation/utils/html_transformer/transform_configuration.dart';
|
|
||||||
|
|
||||||
class LinksTextTransformer extends TextTransformer {
|
|
||||||
|
|
||||||
static final RegExp schemeRegEx = RegExp(r'[a-z]{3,6}://');
|
|
||||||
// Not a perfect but good enough regular expression to match URLs in text.
|
|
||||||
// It also matches a space at the beginning and a dot at the end,
|
|
||||||
// so this is filtered out manually in the found matches
|
|
||||||
static final RegExp linkRegEx = RegExp(
|
|
||||||
r'(([a-z]{3,6}:\/\/)|(^|\s))([a-zA-Z0-9\-]+\.)+[a-z]{2,13}([\?\/]+[\.\?\=\&\%\/\w\+\-]*)?');
|
|
||||||
const LinksTextTransformer();
|
|
||||||
|
|
||||||
@override
|
|
||||||
String transform(String text, String message, TransformConfiguration configuration) {
|
|
||||||
final matches = linkRegEx.allMatches(text);
|
|
||||||
if (matches.isEmpty) {
|
|
||||||
return text;
|
|
||||||
}
|
|
||||||
final buffer = StringBuffer();
|
|
||||||
var end = 0;
|
|
||||||
for (final match in matches) {
|
|
||||||
if (match.end < text.length && text[match.end] == '@') {
|
|
||||||
// this is an email address, abort abort!
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
final originalGroup = match.group(0)!;
|
|
||||||
final group = originalGroup.trimLeft();
|
|
||||||
final start = match.start + originalGroup.length - group.length;
|
|
||||||
buffer.write(text.substring(end, start));
|
|
||||||
final endsWithDot = group.endsWith('.');
|
|
||||||
final urlText =
|
|
||||||
endsWithDot ? group.substring(0, group.length - 1) : group;
|
|
||||||
buffer.write('<a href="');
|
|
||||||
if (!group.startsWith(schemeRegEx)) {
|
|
||||||
buffer.write('https://');
|
|
||||||
}
|
|
||||||
buffer..write(urlText)..write('">')..write(urlText)..write('</a>');
|
|
||||||
end = endsWithDot ? match.end - 1 : match.end;
|
|
||||||
}
|
|
||||||
if (end < text.length) {
|
|
||||||
buffer.write(text.substring(end));
|
|
||||||
}
|
|
||||||
return buffer.toString();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -3,12 +3,7 @@ import 'package:core/presentation/utils/html_transformer/base/dom_transformer.da
|
|||||||
import 'package:core/presentation/utils/html_transformer/base/text_transformer.dart';
|
import 'package:core/presentation/utils/html_transformer/base/text_transformer.dart';
|
||||||
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/link_transformers.dart';
|
|
||||||
import 'package:core/presentation/utils/html_transformer/dom/meta_transformers.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/text/convert_tags_text_transformer.dart';
|
|
||||||
import 'package:core/presentation/utils/html_transformer/text/linebreak_text_transformer.dart';
|
|
||||||
import 'package:core/presentation/utils/html_transformer/text/links_text_transformer.dart';
|
|
||||||
|
|
||||||
/// Contains the configuration for all transformations.
|
/// Contains the configuration for all transformations.
|
||||||
class TransformConfiguration {
|
class TransformConfiguration {
|
||||||
@@ -37,37 +32,26 @@ class TransformConfiguration {
|
|||||||
///
|
///
|
||||||
/// Any specified [customDomTransformers] or [customTextTransformers] are being appended to the standard transformers.
|
/// Any specified [customDomTransformers] or [customTextTransformers] are being appended to the standard transformers.
|
||||||
static TransformConfiguration create({
|
static TransformConfiguration create({
|
||||||
bool? blockExternalImages,
|
|
||||||
int? maxImageWidth,
|
|
||||||
List<DomTransformer>? customDomTransformers,
|
List<DomTransformer>? customDomTransformers,
|
||||||
List<TextTransformer>? customTextTransformers
|
List<TextTransformer>? customTextTransformers
|
||||||
}) {
|
}) {
|
||||||
final domTransformers = (customDomTransformers != null)
|
final domTransformers = (customDomTransformers != null && customDomTransformers.isNotEmpty)
|
||||||
? [...standardDomTransformers, ...customDomTransformers]
|
? [...customDomTransformers]
|
||||||
: [...standardDomTransformers];
|
: [...standardDomTransformers];
|
||||||
final textTransformers = (customTextTransformers != null)
|
final textTransformers = (customTextTransformers != null && customTextTransformers.isNotEmpty)
|
||||||
? [...standardTextTransformers, ...customTextTransformers]
|
? [...customTextTransformers]
|
||||||
: standardTextTransformers;
|
: standardTextTransformers;
|
||||||
maxImageWidth ??= standardMaxImageWidth;
|
|
||||||
return TransformConfiguration(
|
return TransformConfiguration(
|
||||||
domTransformers,
|
domTransformers,
|
||||||
textTransformers
|
textTransformers
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
static const int? standardMaxImageWidth = null;
|
|
||||||
|
|
||||||
static const List<DomTransformer> standardDomTransformers = [
|
static const List<DomTransformer> standardDomTransformers = [
|
||||||
MetaTransformer(),
|
|
||||||
RemoveScriptTransformer(),
|
RemoveScriptTransformer(),
|
||||||
ImageTransformer(),
|
|
||||||
LinkTransformer(),
|
|
||||||
BlockQuotedTransformer(),
|
BlockQuotedTransformer(),
|
||||||
|
ImageTransformer(),
|
||||||
];
|
];
|
||||||
|
|
||||||
static const List<TextTransformer> standardTextTransformers = [
|
static const List<TextTransformer> standardTextTransformers = [];
|
||||||
ConvertTagsTextTransformer(),
|
|
||||||
LinksTextTransformer(),
|
|
||||||
LineBreakTextTransformer(),
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
@@ -3,6 +3,7 @@ import 'dart:convert';
|
|||||||
import 'dart:math' as math;
|
import 'dart:math' as 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_template.dart';
|
||||||
import 'package:core/presentation/views/html_viewer/html_viewer_controller_for_web.dart';
|
import 'package:core/presentation/views/html_viewer/html_viewer_controller_for_web.dart';
|
||||||
import 'package:core/utils/app_logger.dart';
|
import 'package:core/utils/app_logger.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
@@ -36,16 +37,16 @@ class _HtmlContentViewerOnWebState extends State<HtmlContentViewerOnWeb> {
|
|||||||
|
|
||||||
/// The view ID for the IFrameElement. Must be unique.
|
/// The view ID for the IFrameElement. Must be unique.
|
||||||
late String createdViewId;
|
late String createdViewId;
|
||||||
/// The actual height of the editor, used to automatically set the height
|
/// The actual height of the content view, used to automatically set the height
|
||||||
late double actualHeight;
|
late double actualHeight;
|
||||||
/// The actual width of the editor, used to automatically set the width
|
/// The actual width of the content view, used to automatically set the width
|
||||||
late double actualWidth;
|
late double actualWidth;
|
||||||
|
|
||||||
Future<bool>? webInit;
|
Future<bool>? webInit;
|
||||||
String? _htmlData;
|
String? _htmlData;
|
||||||
bool _isLoading = true;
|
bool _isLoading = true;
|
||||||
int minHeight = 100;
|
double minHeight = 100;
|
||||||
int minWidth = 300;
|
double minWidth = 300;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
@@ -64,7 +65,7 @@ class _HtmlContentViewerOnWebState extends State<HtmlContentViewerOnWeb> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
String _generateHtmlDocument(String content) {
|
String _generateHtmlDocument(String content) {
|
||||||
final htmlScripts = '''
|
final webViewActionScripts = '''
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
window.parent.addEventListener('message', handleMessage, false);
|
window.parent.addEventListener('message', handleMessage, false);
|
||||||
window.addEventListener('click', handleOnClickLink, true);
|
window.addEventListener('click', handleOnClickLink, true);
|
||||||
@@ -122,63 +123,11 @@ class _HtmlContentViewerOnWebState extends State<HtmlContentViewerOnWeb> {
|
|||||||
</script>
|
</script>
|
||||||
''';
|
''';
|
||||||
|
|
||||||
final tooltipLinkCss = '''
|
final htmlTemplate = generateHtml(content,
|
||||||
.tooltip .tooltiptext {
|
minHeight: minHeight,
|
||||||
visibility: hidden;
|
minWidth: minWidth,
|
||||||
max-width: 400px;
|
styleCSS: tooltipLinkCss,
|
||||||
background-color: black;
|
javaScripts: webViewActionScripts);
|
||||||
color: #fff;
|
|
||||||
text-align: center;
|
|
||||||
border-radius: 6px;
|
|
||||||
padding: 5px 8px 5px 8px;
|
|
||||||
white-space: nowrap;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
position: absolute;
|
|
||||||
z-index: 1;
|
|
||||||
}
|
|
||||||
.tooltip:hover .tooltiptext {
|
|
||||||
visibility: visible;
|
|
||||||
}
|
|
||||||
''';
|
|
||||||
|
|
||||||
final htmlTemplate = '''
|
|
||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
|
|
||||||
<style>
|
|
||||||
#editor {
|
|
||||||
outline: 0px solid transparent;
|
|
||||||
min-height: ${minHeight}px;
|
|
||||||
min-width: ${minWidth}px;
|
|
||||||
color: #000000;
|
|
||||||
font-family: Inter;
|
|
||||||
font-size: 16px;
|
|
||||||
font-style: normal;
|
|
||||||
}
|
|
||||||
table {
|
|
||||||
width: 100%;
|
|
||||||
max-width: 100%;
|
|
||||||
}
|
|
||||||
td {
|
|
||||||
padding: 13px;
|
|
||||||
margin: 0px;
|
|
||||||
}
|
|
||||||
th {
|
|
||||||
padding: 13px;
|
|
||||||
margin: 0px;
|
|
||||||
}
|
|
||||||
$tooltipLinkCss
|
|
||||||
</style>
|
|
||||||
$htmlScripts
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div id="editor">$content</div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
''';
|
|
||||||
|
|
||||||
return htmlTemplate;
|
return htmlTemplate;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,7 +43,8 @@ class _HtmlContentViewState extends State<HtmlContentViewer> {
|
|||||||
|
|
||||||
double? _webViewHeight = 1.0;
|
double? _webViewHeight = 1.0;
|
||||||
double? _webViewWidth = 1.0;
|
double? _webViewWidth = 1.0;
|
||||||
int minHeight = 100;
|
double minHeight = 100;
|
||||||
|
double minWidth = 300;
|
||||||
String? _htmlData;
|
String? _htmlData;
|
||||||
late WebViewController _webViewController;
|
late WebViewController _webViewController;
|
||||||
bool _isLoading = true;
|
bool _isLoading = true;
|
||||||
@@ -56,47 +57,7 @@ class _HtmlContentViewState extends State<HtmlContentViewer> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
String _generateHtmlDocument(String content) {
|
String _generateHtmlDocument(String content) {
|
||||||
final htmlTemplate = '''
|
final htmlTemplate = generateHtml(content);
|
||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
|
|
||||||
<style>
|
|
||||||
#editor {
|
|
||||||
outline: 0px solid transparent;
|
|
||||||
min-height: ${minHeight}px;
|
|
||||||
min-width: 300px;
|
|
||||||
color: #182952;
|
|
||||||
font-family: verdana;
|
|
||||||
}
|
|
||||||
table {
|
|
||||||
width: 100%;
|
|
||||||
max-width: 100%;
|
|
||||||
}
|
|
||||||
td {
|
|
||||||
padding: 13px;
|
|
||||||
margin: 0px;
|
|
||||||
}
|
|
||||||
th {
|
|
||||||
padding: 13px;
|
|
||||||
margin: 0px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
<script>
|
|
||||||
var documentHeight;
|
|
||||||
|
|
||||||
function onLoaded() {
|
|
||||||
documentHeight = document.body.scrollHeight;
|
|
||||||
document.execCommand("styleWithCSS", false, true);
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
</head>
|
|
||||||
<body onload="onLoaded();">
|
|
||||||
<div id="editor">$content</div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
''';
|
|
||||||
return htmlTemplate;
|
return htmlTemplate;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,4 +6,6 @@ abstract class HtmlDataSource {
|
|||||||
EmailContent emailContent,
|
EmailContent emailContent,
|
||||||
Map<String, String> mapUrlDownloadCID
|
Map<String, String> mapUrlDownloadCID
|
||||||
);
|
);
|
||||||
|
|
||||||
|
Future<EmailContent> addTooltipWhenHoverOnLink(EmailContent emailContent);
|
||||||
}
|
}
|
||||||
@@ -21,4 +21,13 @@ class HtmlDataSourceImpl extends HtmlDataSource {
|
|||||||
throw error;
|
throw error;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<EmailContent> addTooltipWhenHoverOnLink(EmailContent emailContent) {
|
||||||
|
return Future.sync(() async {
|
||||||
|
return await _htmlAnalyzer.addTooltipWhenHoverOnLink(emailContent);
|
||||||
|
}).catchError((error) {
|
||||||
|
throw error;
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
|
|
||||||
import 'package:core/core.dart';
|
import 'package:core/core.dart';
|
||||||
import 'package:core/presentation/utils/html_transformer/html_transform.dart';
|
|
||||||
import 'package:model/model.dart';
|
import 'package:model/model.dart';
|
||||||
|
|
||||||
class HtmlAnalyzer {
|
class HtmlAnalyzer {
|
||||||
@@ -14,12 +13,24 @@ class HtmlAnalyzer {
|
|||||||
case EmailContentType.textHtml:
|
case EmailContentType.textHtml:
|
||||||
final htmlTransform = HtmlTransform(
|
final htmlTransform = HtmlTransform(
|
||||||
emailContent.content,
|
emailContent.content,
|
||||||
dioClient,
|
dioClient: dioClient,
|
||||||
mapUrlDownloadCID);
|
mapUrlDownloadCID: mapUrlDownloadCID);
|
||||||
final htmlContent = await htmlTransform.transformToHtml();
|
final htmlContent = await htmlTransform.transformToHtml();
|
||||||
return EmailContent(emailContent.type, htmlContent);
|
return EmailContent(emailContent.type, htmlContent);
|
||||||
default:
|
default:
|
||||||
return emailContent;
|
return emailContent;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<EmailContent> addTooltipWhenHoverOnLink(EmailContent emailContent) async {
|
||||||
|
switch(emailContent.type) {
|
||||||
|
case EmailContentType.textHtml:
|
||||||
|
final htmlTransform = HtmlTransform(emailContent.content);
|
||||||
|
final htmlContent = await htmlTransform.transformToHtml(
|
||||||
|
transformConfiguration: TransformConfiguration.create(customDomTransformers: [AddTooltipLinkTransformer()]));
|
||||||
|
return EmailContent(emailContent.type, htmlContent);
|
||||||
|
default:
|
||||||
|
return emailContent;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -124,4 +124,11 @@ class EmailRepositoryImpl extends EmailRepository {
|
|||||||
Future<bool> deleteEmailPermanently(AccountId accountId, EmailId emailId) {
|
Future<bool> deleteEmailPermanently(AccountId accountId, EmailId emailId) {
|
||||||
return emailDataSource.deleteEmailPermanently(accountId, emailId);
|
return emailDataSource.deleteEmailPermanently(accountId, emailId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<List<EmailContent>> addTooltipWhenHoverOnLink(List<EmailContent> emailContents) {
|
||||||
|
return Future.wait(emailContents
|
||||||
|
.map((emailContent) => _htmlDataSource.addTooltipWhenHoverOnLink(emailContent))
|
||||||
|
.toList());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -52,6 +52,8 @@ abstract class EmailRepository {
|
|||||||
AccountId accountId
|
AccountId accountId
|
||||||
);
|
);
|
||||||
|
|
||||||
|
Future<List<EmailContent>> addTooltipWhenHoverOnLink(List<EmailContent> emailContents);
|
||||||
|
|
||||||
Future<Email?> saveEmailAsDrafts(AccountId accountId, Email email);
|
Future<Email?> saveEmailAsDrafts(AccountId accountId, Email email);
|
||||||
|
|
||||||
Future<bool> removeEmailDrafts(AccountId accountId, EmailId emailId);
|
Future<bool> removeEmailDrafts(AccountId accountId, EmailId emailId);
|
||||||
|
|||||||
@@ -3,12 +3,13 @@ import 'package:model/model.dart';
|
|||||||
|
|
||||||
class GetEmailContentSuccess extends UIState {
|
class GetEmailContentSuccess extends UIState {
|
||||||
final List<EmailContent> emailContents;
|
final List<EmailContent> emailContents;
|
||||||
|
final List<EmailContent> emailContentsDisplayed;
|
||||||
final List<Attachment> attachments;
|
final List<Attachment> attachments;
|
||||||
|
|
||||||
GetEmailContentSuccess(this.emailContents, this.attachments);
|
GetEmailContentSuccess(this.emailContents, this.emailContentsDisplayed, this.attachments);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<Object?> get props => [emailContents, attachments];
|
List<Object?> get props => [emailContents, emailContentsDisplayed, attachments];
|
||||||
}
|
}
|
||||||
|
|
||||||
class GetEmailContentFailure extends FeatureFailure {
|
class GetEmailContentFailure extends FeatureFailure {
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import 'package:core/core.dart';
|
import 'package:core/core.dart';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:model/model.dart';
|
import 'package:model/model.dart';
|
||||||
import 'package:dartz/dartz.dart';
|
import 'package:dartz/dartz.dart';
|
||||||
import 'package:jmap_dart_client/jmap/account_id.dart';
|
import 'package:jmap_dart_client/jmap/account_id.dart';
|
||||||
@@ -22,7 +23,10 @@ class GetEmailContentInteractor {
|
|||||||
email.allAttachments.listAttachmentsDisplayedInContent,
|
email.allAttachments.listAttachmentsDisplayedInContent,
|
||||||
baseDownloadUrl,
|
baseDownloadUrl,
|
||||||
accountId);
|
accountId);
|
||||||
yield Right<Failure, Success>(GetEmailContentSuccess(newEmailContents, email.allAttachments));
|
final newEmailContentsDisplayed = kIsWeb
|
||||||
|
? await emailRepository.addTooltipWhenHoverOnLink(newEmailContents)
|
||||||
|
: newEmailContents;
|
||||||
|
yield Right<Failure, Success>(GetEmailContentSuccess(newEmailContents, newEmailContentsDisplayed, email.allAttachments));
|
||||||
} else {
|
} else {
|
||||||
yield Left(GetEmailContentFailure(null));
|
yield Left(GetEmailContentFailure(null));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -65,6 +65,7 @@ class EmailController extends BaseController {
|
|||||||
final emailContents = <EmailContent>[].obs;
|
final emailContents = <EmailContent>[].obs;
|
||||||
final attachments = <Attachment>[].obs;
|
final attachments = <Attachment>[].obs;
|
||||||
EmailId? _currentEmailId;
|
EmailId? _currentEmailId;
|
||||||
|
List<EmailContent>? initialEmailContents;
|
||||||
|
|
||||||
PresentationMailbox? get currentMailbox => mailboxDashBoardController.selectedMailbox.value;
|
PresentationMailbox? get currentMailbox => mailboxDashBoardController.selectedMailbox.value;
|
||||||
|
|
||||||
@@ -152,7 +153,8 @@ class EmailController extends BaseController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _getEmailContentSuccess(GetEmailContentSuccess success) {
|
void _getEmailContentSuccess(GetEmailContentSuccess success) {
|
||||||
emailContents.value = success.emailContents;
|
emailContents.value = success.emailContentsDisplayed;
|
||||||
|
initialEmailContents = success.emailContents;
|
||||||
attachments.value = success.attachments;
|
attachments.value = success.attachments;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -161,6 +163,7 @@ class EmailController extends BaseController {
|
|||||||
emailAddressExpandMode.value = ExpandMode.COLLAPSE;
|
emailAddressExpandMode.value = ExpandMode.COLLAPSE;
|
||||||
isDisplayFullEmailAddress.value = false;
|
isDisplayFullEmailAddress.value = false;
|
||||||
emailContents.clear();
|
emailContents.clear();
|
||||||
|
initialEmailContents?.clear();
|
||||||
attachments.clear();
|
attachments.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -592,7 +595,7 @@ class EmailController extends BaseController {
|
|||||||
final arguments = ComposerArguments(
|
final arguments = ComposerArguments(
|
||||||
emailActionType: emailActionType,
|
emailActionType: emailActionType,
|
||||||
presentationEmail: mailboxDashBoardController.selectedEmail.value!,
|
presentationEmail: mailboxDashBoardController.selectedEmail.value!,
|
||||||
emailContents: emailContents,
|
emailContents: initialEmailContents,
|
||||||
attachments: attachments,
|
attachments: attachments,
|
||||||
mailboxRole: mailboxDashBoardController.selectedMailbox.value?.role);
|
mailboxRole: mailboxDashBoardController.selectedMailbox.value?.role);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user