TF-2948 Apply new identity view for web

Signed-off-by: dab246 <tdvu@linagora.com>
This commit is contained in:
dab246
2025-07-30 19:09:33 +07:00
committed by Dat H. Pham
parent 02a7adc64a
commit 23ecdfbf11
61 changed files with 963 additions and 804 deletions
+1
View File
@@ -69,6 +69,7 @@ export 'presentation/views/button/icon_button_web.dart';
export 'presentation/views/button/tmail_button_widget.dart';
export 'presentation/views/image/avatar_builder.dart';
export 'presentation/views/list/sliver_grid_delegate_fixed_height.dart';
export 'presentation/views/list/no_stretch_scroll_behavior.dart';
export 'presentation/views/image/icon_builder.dart';
export 'presentation/views/context_menu/context_menu_action_builder.dart';
export 'presentation/views/context_menu/context_menu_builder.dart';
@@ -2,6 +2,7 @@ class ConstantsUI {
static const String fontApp = 'Inter';
static const double htmlContentMaxHeight = 22000.0;
static const double composerHtmlContentMaxHeight = 20000.0;
static const double htmlContentMinWidth = 300;
static const double htmlContentMinHeight = 150;
static const double htmlContentOffsetHeight = 30.0;
}
@@ -232,6 +232,8 @@ class ImagePaths {
String get icRefreshQuotas => _getImagePath('ic_refresh_quotas.svg');
String get icCreateFilter => _getImagePath('ic_create_filter.svg');
String get icArrowBack => _getImagePath('ic_arrow_back.svg');
String get icRadio => _getImagePath('ic_radio.svg');
String get icRadioSelected => _getImagePath('ic_radio_selected.svg');
String get icTwakeWorkplace => _getIconPath('icon_twp.png');
@@ -92,6 +92,16 @@ class TransformConfiguration {
const RemoveMaxWidthInImageStyleTransformer(),
]);
factory TransformConfiguration.forSignatureIdentity() => TransformConfiguration.create(
customDomTransformers: [
const RemoveScriptTransformer(),
const BlockQuotedTransformer(),
const BlockCodeTransformer(),
SanitizeHyperLinkTagInHtmlTransformer(useTooltip: PlatformInfo.isWeb),
const ImageTransformer(),
],
);
/// Provides easy access to a standard configuration that does not block external images.
static TransformConfiguration standardConfiguration = TransformConfiguration(
standardDomTransformers,
@@ -2,6 +2,7 @@ import 'dart:async';
import 'dart:convert';
import 'dart:math' as math;
import 'package:core/presentation/constants/constants_ui.dart';
import 'package:core/presentation/extensions/color_extension.dart';
import 'package:core/presentation/utils/shims/dart_ui.dart' as ui;
import 'package:core/utils/app_logger.dart';
@@ -9,6 +10,7 @@ import 'package:core/utils/html/html_interaction.dart';
import 'package:core/utils/html/html_template.dart';
import 'package:core/utils/html/html_utils.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:universal_html/html.dart' as html;
typedef OnClickHyperLinkAction = Function(Uri?);
@@ -34,6 +36,13 @@ class HtmlContentViewerOnWeb extends StatefulWidget {
final bool keepWidthWhileLoading;
final ScrollController? scrollController;
final bool enableQuoteToggle;
final bool disableScrolling;
final bool keepAlive;
final double htmlContentMinHeight;
final double htmlContentMinWidth;
final double offsetHtmlContentHeight;
final double? viewMaxHeight;
final bool autoAdjustHeight;
const HtmlContentViewerOnWeb({
Key? key,
@@ -49,15 +58,21 @@ class HtmlContentViewerOnWeb extends StatefulWidget {
this.contentPadding,
this.scrollController,
this.enableQuoteToggle = false,
this.keepAlive = false,
this.disableScrolling = false,
this.autoAdjustHeight = false,
this.htmlContentMinHeight = ConstantsUI.htmlContentMinHeight,
this.htmlContentMinWidth = ConstantsUI.htmlContentMinWidth,
this.offsetHtmlContentHeight = ConstantsUI.htmlContentOffsetHeight,
this.viewMaxHeight,
}) : super(key: key);
@override
State<HtmlContentViewerOnWeb> createState() => _HtmlContentViewerOnWebState();
}
class _HtmlContentViewerOnWebState extends State<HtmlContentViewerOnWeb> {
static const double _minWidth = 300;
class _HtmlContentViewerOnWebState extends State<HtmlContentViewerOnWeb>
with AutomaticKeepAliveClientMixin {
/// The view ID for the IFrameElement. Must be unique.
late String _createdViewId;
/// The actual height of the content view, used to automatically set the height
@@ -68,7 +83,7 @@ class _HtmlContentViewerOnWebState extends State<HtmlContentViewerOnWeb> {
Future<bool>? _webInit;
String? _htmlData;
bool _isLoading = true;
double minHeight = 100;
late double minHeight;
late final StreamSubscription<html.MessageEvent> _onMessageSubscription;
bool _iframeLoaded = false;
static const String iframeOnLoadMessage = 'iframeHasBeenLoaded';
@@ -80,7 +95,7 @@ class _HtmlContentViewerOnWebState extends State<HtmlContentViewerOnWeb> {
super.initState();
_actualHeight = widget.heightContent;
_actualWidth = widget.widthContent;
_createdViewId = _getRandString(10);
minHeight = widget.htmlContentMinHeight;
_setUpWeb();
_onMessageSubscription = html.window.onMessage.listen(_handleMessageEvent);
}
@@ -119,6 +134,7 @@ class _HtmlContentViewerOnWebState extends State<HtmlContentViewerOnWeb> {
bool _isScrollChangedEventTriggered(String? type) {
return widget.scrollController != null &&
widget.scrollController?.hasClients == true &&
type?.contains('toDart: $onScrollChangedEvent') == true;
}
@@ -144,8 +160,13 @@ class _HtmlContentViewerOnWebState extends State<HtmlContentViewerOnWeb> {
void _handleContentHeightEvent(dynamic height) {
final docHeight = height ?? _actualHeight;
if (docHeight != null && mounted) {
final scrollHeightWithBuffer = docHeight + 30.0;
if (scrollHeightWithBuffer > minHeight) {
final scrollHeightWithBuffer = docHeight + widget.offsetHtmlContentHeight;
log('$runtimeType::_handleContentHeightEvent: ScrollHeightWithBuffer = $scrollHeightWithBuffer');
bool isHeightChanged = widget.autoAdjustHeight
? scrollHeightWithBuffer >= minHeight
: scrollHeightWithBuffer > minHeight;
if (isHeightChanged) {
setState(() {
_actualHeight = scrollHeightWithBuffer;
_isLoading = false;
@@ -166,7 +187,7 @@ class _HtmlContentViewerOnWebState extends State<HtmlContentViewerOnWeb> {
void _handleContentWidthEvent(dynamic width) {
final docWidth = width ?? _actualWidth;
if (docWidth != null && mounted &&
docWidth > _minWidth &&
docWidth > widget.htmlContentMinWidth &&
widget.allowResizeToDocumentSize) {
setState(() => _actualWidth = docWidth);
}
@@ -196,7 +217,6 @@ class _HtmlContentViewerOnWebState extends State<HtmlContentViewerOnWeb> {
log('_HtmlContentViewerOnWebState::didUpdateWidget():Old-Direction: ${oldWidget.direction} | Current-Direction: ${widget.direction}');
if (widget.contentHtml != oldWidget.contentHtml ||
widget.direction != oldWidget.direction) {
_createdViewId = _getRandString(10);
_setUpWeb();
}
@@ -247,10 +267,12 @@ class _HtmlContentViewerOnWebState extends State<HtmlContentViewerOnWeb> {
}
}
const resizeObserver = new ResizeObserver((entries) => {
var height = document.body.scrollHeight;
window.parent.postMessage(JSON.stringify({"view": "$_createdViewId", "type": "toDart: htmlHeight", "height": height}), "*");
});
${!widget.autoAdjustHeight ? '''
const resizeObserver = new ResizeObserver((entries) => {
var height = document.body.scrollHeight;
window.parent.postMessage(JSON.stringify({"view": "$_createdViewId", "type": "toDart: htmlHeight", "height": height}), "*");
});
''' : ''}
${widget.mailtoDelegate != null
? '''
@@ -297,7 +319,7 @@ class _HtmlContentViewerOnWebState extends State<HtmlContentViewerOnWeb> {
'''
: ''}
resizeObserver.observe(document.body);
${!widget.autoAdjustHeight ? 'resizeObserver.observe(document.body);' : ''}
}
${widget.scrollController != null ? '''
@@ -313,39 +335,30 @@ class _HtmlContentViewerOnWebState extends State<HtmlContentViewerOnWeb> {
</script>
''';
const scriptsDisableZoom = '''
<script type="text/javascript">
document.addEventListener('wheel', function(e) {
e.ctrlKey && e.preventDefault();
}, {
passive: false,
});
window.addEventListener('keydown', function(e) {
if (event.metaKey || event.ctrlKey) {
switch (event.key) {
case '=':
case '-':
event.preventDefault();
break;
}
}
});
</script>
''';
final processedContent = widget.enableQuoteToggle
? HtmlUtils.addQuoteToggle(content)
: content;
final combinedCss = [
HtmlTemplate.tooltipLinkCss,
if (widget.enableQuoteToggle) HtmlUtils.quoteToggleStyle,
if (widget.disableScrolling) HtmlTemplate.disableScrollingStyleCSS,
].join();
final combinedScripts = [
webViewActionScripts,
HtmlInteraction.scriptsDisableZoom,
HtmlInteraction.scriptsHandleLazyLoadingBackgroundImage,
HtmlInteraction.generateNormalizeImageScript(widget.widthContent),
if (widget.enableQuoteToggle) HtmlUtils.quoteToggleScript,
].join();
final htmlTemplate = HtmlUtils.generateHtmlDocument(
content: widget.enableQuoteToggle
? HtmlUtils.addQuoteToggle(content)
: content,
content: processedContent,
minHeight: minHeight,
minWidth: _minWidth,
styleCSS: HtmlTemplate.tooltipLinkCss
+ (widget.enableQuoteToggle ? HtmlUtils.quoteToggleStyle : ''),
javaScripts: webViewActionScripts
+ scriptsDisableZoom
+ HtmlInteraction.scriptsHandleLazyLoadingBackgroundImage
+ HtmlInteraction.generateNormalizeImageScript(widget.widthContent)
+ (widget.enableQuoteToggle ? HtmlUtils.quoteToggleScript : ''),
minWidth: widget.htmlContentMinWidth,
styleCSS: combinedCss,
javaScripts: combinedScripts,
direction: widget.direction,
contentPadding: widget.contentPadding,
useDefaultFont: widget.useDefaultFont,
@@ -355,6 +368,7 @@ class _HtmlContentViewerOnWebState extends State<HtmlContentViewerOnWeb> {
}
void _setUpWeb() {
_createdViewId = _getRandString(10);
_htmlData = _generateHtmlDocument(widget.contentHtml);
final iframe = html.IFrameElement()
@@ -377,54 +391,76 @@ class _HtmlContentViewerOnWebState extends State<HtmlContentViewerOnWeb> {
@override
Widget build(BuildContext context) {
return LayoutBuilder(builder: (context, constraint) {
minHeight = math.min(constraint.maxHeight, minHeight);
final child = Stack(
children: [
if (_htmlData?.isNotEmpty == false)
const SizedBox.shrink()
else
FutureBuilder<bool>(
future: _webInit,
builder: (context, snapshot) {
if (snapshot.hasData) {
super.build(context);
if (widget.autoAdjustHeight) {
return _buildHtmlElementView();
} else {
return LayoutBuilder(
builder: (_, constraint) {
minHeight = math.min(constraint.maxHeight, minHeight);
return _buildHtmlElementView();
},
);
}
}
Widget _buildHtmlElementView() {
log('$runtimeType::_buildHtmlElementView: ActualHeight: $_actualHeight');
final child = Stack(
children: [
if (_htmlData?.trim().isNotEmpty == true)
FutureBuilder<bool>(
future: _webInit,
builder: (_, snapshot) {
if (snapshot.hasData) {
final htmlView = HtmlElementView(
key: ValueKey('$_htmlData-${widget.key}'),
viewType: _createdViewId,
);
if (widget.viewMaxHeight != null) {
return Container(
height: _actualHeight,
width: _actualWidth,
color: Colors.greenAccent,
constraints: BoxConstraints(
maxHeight: widget.viewMaxHeight!,
),
child: htmlView,
);
} else {
return SizedBox(
height: _actualHeight,
width: _actualWidth,
child: HtmlElementView(
key: ValueKey('$_htmlData-${widget.key}'),
viewType: _createdViewId,
),
child: htmlView,
);
} else {
return const SizedBox.shrink();
}
} else {
return const SizedBox.shrink();
}
},
),
if (_isLoading)
const Align(
alignment: Alignment.topCenter,
child: Padding(
padding: EdgeInsets.all(16),
child: SizedBox(
width: 30,
height: 30,
child: CupertinoActivityIndicator(color: AppColor.colorLoading),
),
),
if (_isLoading)
const Align(
alignment: Alignment.topCenter,
child: Padding(
padding: EdgeInsets.all(16),
child: SizedBox(
width: 30,
height: 30,
child: CupertinoActivityIndicator(
color: AppColor.colorLoading
)
)
)
)
],
);
),
],
);
if (!widget.keepWidthWhileLoading) return child;
return SizedBox(
width: _actualWidth,
child: child,
);
});
if (widget.keepWidthWhileLoading) {
return child;
} else {
return SizedBox(width: _actualWidth, child: child);
}
}
@override
@@ -433,4 +469,7 @@ class _HtmlContentViewerOnWebState extends State<HtmlContentViewerOnWeb> {
_onMessageSubscription.cancel();
super.dispose();
}
@override
bool get wantKeepAlive => widget.keepAlive;
}
@@ -0,0 +1,12 @@
import 'package:flutter/material.dart';
class NoStretchScrollBehavior extends ScrollBehavior {
@override
Widget buildOverscrollIndicator(
BuildContext context,
Widget child,
ScrollableDetails details,
) {
return child;
}
}
+20
View File
@@ -278,4 +278,24 @@ class HtmlInteraction {
</script>
''';
}
static const scriptsDisableZoom = '''
<script type="text/javascript">
document.addEventListener('wheel', function(e) {
e.ctrlKey && e.preventDefault();
}, {
passive: false,
});
window.addEventListener('keydown', function(e) {
if (event.metaKey || event.ctrlKey) {
switch (event.key) {
case '=':
case '-':
event.preventDefault();
break;
}
}
});
</script>
''';
}
+12
View File
@@ -250,4 +250,16 @@ class HtmlTemplate {
}
</style>
''';
static const String disableScrollingStyleCSS = '''
html, body {
overflow: hidden;
overscroll-behavior: none;
scrollbar-width: none; /* Firefox */
-ms-overflow-style: none; /* IE/Edge */
}
::-webkit-scrollbar {
display: none;
}
''';
}