TF-2948 Apply new identity view for web
Signed-off-by: dab246 <tdvu@linagora.com>
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
''';
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
''';
|
||||
}
|
||||
Reference in New Issue
Block a user