TF-4084 Fix cannot read or scroll emails when opening them in Twake Mail on Android chrome mobile browser
This commit is contained in:
@@ -8,6 +8,7 @@ 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_template.dart';
|
import 'package:core/utils/html/html_template.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:flutter/cupertino.dart';
|
import 'package:flutter/cupertino.dart';
|
||||||
import 'package:universal_html/html.dart' as html;
|
import 'package:universal_html/html.dart' as html;
|
||||||
|
|
||||||
@@ -87,6 +88,7 @@ class _HtmlContentViewerOnWebState extends State<HtmlContentViewerOnWeb>
|
|||||||
static const String iframeOnLoadMessage = 'iframeHasBeenLoaded';
|
static const String iframeOnLoadMessage = 'iframeHasBeenLoaded';
|
||||||
static const String onClickHyperLinkName = 'onClickHyperLink';
|
static const String onClickHyperLinkName = 'onClickHyperLink';
|
||||||
static const String onScrollChangedEvent = 'onScrollChanged';
|
static const String onScrollChangedEvent = 'onScrollChanged';
|
||||||
|
static const String onScrollEndEvent = 'onScrollEnd';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
@@ -106,9 +108,12 @@ class _HtmlContentViewerOnWebState extends State<HtmlContentViewerOnWeb>
|
|||||||
if (viewId != _createdViewId) return;
|
if (viewId != _createdViewId) return;
|
||||||
|
|
||||||
final type = data['type'];
|
final type = data['type'];
|
||||||
if (_isScrollChangedEventTriggered(type)) {
|
if (_isScrollingIsAvailable && _isScrollChangedEventTriggered(type)) {
|
||||||
_handleIframeOnScrollChangedListener(data, widget.scrollController!);
|
_handleIframeOnScrollChangedListener(data, widget.scrollController!);
|
||||||
return;
|
return;
|
||||||
|
} else if (_isScrollingIsAvailable && _isScrollEndEventTriggered(type)) {
|
||||||
|
_handleIframeOnScrollEndListener(data, widget.scrollController!);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data['message'] == iframeOnLoadMessage) {
|
if (data['message'] == iframeOnLoadMessage) {
|
||||||
@@ -130,10 +135,17 @@ class _HtmlContentViewerOnWebState extends State<HtmlContentViewerOnWeb>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool _isScrollChangedEventTriggered(String? type) {
|
bool get _isScrollingIsAvailable {
|
||||||
return widget.scrollController != null &&
|
return widget.scrollController != null &&
|
||||||
widget.scrollController?.hasClients == true &&
|
widget.scrollController?.hasClients == true;
|
||||||
type?.contains('toDart: $onScrollChangedEvent') == true;
|
}
|
||||||
|
|
||||||
|
bool _isScrollChangedEventTriggered(String? type) {
|
||||||
|
return type?.contains('toDart: $onScrollChangedEvent') == true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _isScrollEndEventTriggered(String? type) {
|
||||||
|
return type?.contains('toDart: $onScrollEndEvent') == true;
|
||||||
}
|
}
|
||||||
|
|
||||||
void _handleIframeOnScrollChangedListener(
|
void _handleIframeOnScrollChangedListener(
|
||||||
@@ -141,17 +153,51 @@ class _HtmlContentViewerOnWebState extends State<HtmlContentViewerOnWeb>
|
|||||||
ScrollController controller,
|
ScrollController controller,
|
||||||
) {
|
) {
|
||||||
final deltaY = data['deltaY'] ?? 0.0;
|
final deltaY = data['deltaY'] ?? 0.0;
|
||||||
final newOffset = controller.offset + deltaY;
|
final target = controller.offset + deltaY;
|
||||||
log('_HtmlContentViewerOnWebState::_handleIframeOnScrollChangedListener:deltaY = $deltaY | newOffset = $newOffset');
|
|
||||||
if (newOffset < controller.position.minScrollExtent) {
|
if (PlatformInfo.isWebTouchDevice) {
|
||||||
controller.jumpTo(controller.position.minScrollExtent);
|
final newOffset = target.clamp(
|
||||||
} else if (newOffset > controller.position.maxScrollExtent) {
|
controller.position.minScrollExtent,
|
||||||
controller.jumpTo(controller.position.maxScrollExtent);
|
controller.position.maxScrollExtent,
|
||||||
|
);
|
||||||
|
|
||||||
|
controller.animateTo(
|
||||||
|
newOffset,
|
||||||
|
duration: const Duration(milliseconds: 50),
|
||||||
|
curve: Curves.linear,
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
controller.jumpTo(newOffset);
|
if (target < controller.position.minScrollExtent) {
|
||||||
|
controller.jumpTo(controller.position.minScrollExtent);
|
||||||
|
} else if (target > controller.position.maxScrollExtent) {
|
||||||
|
controller.jumpTo(controller.position.maxScrollExtent);
|
||||||
|
} else {
|
||||||
|
controller.jumpTo(target);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _handleIframeOnScrollEndListener(
|
||||||
|
dynamic data,
|
||||||
|
ScrollController controller,
|
||||||
|
) {
|
||||||
|
final velocity = data['velocity'] ?? 0.0;
|
||||||
|
|
||||||
|
final distance = velocity * 800;
|
||||||
|
final target = controller.offset + distance;
|
||||||
|
|
||||||
|
final newOffset = target.clamp(
|
||||||
|
controller.position.minScrollExtent,
|
||||||
|
controller.position.maxScrollExtent,
|
||||||
|
);
|
||||||
|
|
||||||
|
controller.animateTo(
|
||||||
|
newOffset,
|
||||||
|
duration: const Duration(milliseconds: 600),
|
||||||
|
curve: Curves.decelerate,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
bool _isHtmlContentHeightEventTriggered(String? type) =>
|
bool _isHtmlContentHeightEventTriggered(String? type) =>
|
||||||
type?.contains('toDart: htmlHeight') == true;
|
type?.contains('toDart: htmlHeight') == true;
|
||||||
|
|
||||||
@@ -319,17 +365,6 @@ class _HtmlContentViewerOnWebState extends State<HtmlContentViewerOnWeb>
|
|||||||
|
|
||||||
${!widget.autoAdjustHeight ? 'resizeObserver.observe(document.body);' : ''}
|
${!widget.autoAdjustHeight ? 'resizeObserver.observe(document.body);' : ''}
|
||||||
}
|
}
|
||||||
|
|
||||||
${widget.scrollController != null ? '''
|
|
||||||
window.addEventListener('wheel', function (event) {
|
|
||||||
const deltaY = event.deltaY;
|
|
||||||
window.parent.postMessage(JSON.stringify({
|
|
||||||
"view": "$_createdViewId",
|
|
||||||
"type": "toDart: $onScrollChangedEvent",
|
|
||||||
"deltaY": deltaY
|
|
||||||
}), "*");
|
|
||||||
});
|
|
||||||
''' : ''}
|
|
||||||
</script>
|
</script>
|
||||||
''';
|
''';
|
||||||
|
|
||||||
@@ -349,6 +384,17 @@ class _HtmlContentViewerOnWebState extends State<HtmlContentViewerOnWeb>
|
|||||||
HtmlInteraction.scriptsHandleLazyLoadingBackgroundImage,
|
HtmlInteraction.scriptsHandleLazyLoadingBackgroundImage,
|
||||||
HtmlInteraction.generateNormalizeImageScript(widget.widthContent),
|
HtmlInteraction.generateNormalizeImageScript(widget.widthContent),
|
||||||
if (widget.enableQuoteToggle) HtmlUtils.quoteToggleScript,
|
if (widget.enableQuoteToggle) HtmlUtils.quoteToggleScript,
|
||||||
|
if (widget.scrollController != null)
|
||||||
|
PlatformInfo.isWebTouchDevice
|
||||||
|
? HtmlInteraction.scriptsTouchEventListener(
|
||||||
|
viewId: _createdViewId,
|
||||||
|
onScrollChangedEvent: onScrollChangedEvent,
|
||||||
|
onScrollEndEvent: onScrollEndEvent,
|
||||||
|
)
|
||||||
|
: HtmlInteraction.scriptsWheelEventListener(
|
||||||
|
viewId: _createdViewId,
|
||||||
|
onScrollChangedEvent: onScrollChangedEvent,
|
||||||
|
),
|
||||||
].join();
|
].join();
|
||||||
|
|
||||||
final htmlTemplate = HtmlUtils.generateHtmlDocument(
|
final htmlTemplate = HtmlUtils.generateHtmlDocument(
|
||||||
|
|||||||
@@ -298,4 +298,84 @@ class HtmlInteraction {
|
|||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
''';
|
''';
|
||||||
|
|
||||||
|
static String scriptsWheelEventListener({
|
||||||
|
required String viewId,
|
||||||
|
required String onScrollChangedEvent,
|
||||||
|
}) => '''
|
||||||
|
<script type="text/javascript">
|
||||||
|
function onWheel(e) {
|
||||||
|
const deltaY = event.deltaY;
|
||||||
|
window.parent.postMessage(JSON.stringify({
|
||||||
|
"view": "$viewId",
|
||||||
|
"type": "toDart: $onScrollChangedEvent",
|
||||||
|
"deltaY": deltaY
|
||||||
|
}), "*");
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener('wheel', onWheel, { passive: true });
|
||||||
|
|
||||||
|
window.addEventListener('pagehide', (event) => {
|
||||||
|
window.removeEventListener('wheel', onWheel);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
''';
|
||||||
|
|
||||||
|
static String scriptsTouchEventListener({
|
||||||
|
required String viewId,
|
||||||
|
required String onScrollChangedEvent,
|
||||||
|
required String onScrollEndEvent,
|
||||||
|
}) => '''
|
||||||
|
<script type="text/javascript">
|
||||||
|
let lastY = 0;
|
||||||
|
let lastTime = 0;
|
||||||
|
let velocity = 0;
|
||||||
|
|
||||||
|
function onTouchStart(e) {
|
||||||
|
lastY = e.touches[0].clientY;
|
||||||
|
lastTime = performance.now();
|
||||||
|
velocity = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function onTouchMove(e) {
|
||||||
|
const now = performance.now();
|
||||||
|
const y = e.touches[0].clientY;
|
||||||
|
const dy = lastY - y;
|
||||||
|
const dt = now - lastTime;
|
||||||
|
|
||||||
|
if (dt > 0) {
|
||||||
|
velocity = dy / dt; // px per ms
|
||||||
|
velocity = Math.max(Math.min(velocity, 2), -2); // clamp velocity
|
||||||
|
}
|
||||||
|
|
||||||
|
lastY = y;
|
||||||
|
lastTime = now;
|
||||||
|
|
||||||
|
window.parent.postMessage(JSON.stringify({
|
||||||
|
view: "$viewId",
|
||||||
|
type: "toDart: $onScrollChangedEvent",
|
||||||
|
deltaY: dy,
|
||||||
|
}), '*');
|
||||||
|
}
|
||||||
|
|
||||||
|
function onTouchEnd(e) {
|
||||||
|
window.parent.postMessage(JSON.stringify({
|
||||||
|
view: "$viewId",
|
||||||
|
type: "toDart: $onScrollEndEvent",
|
||||||
|
velocity: velocity,
|
||||||
|
}), '*');
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener('touchstart', onTouchStart, { passive: true });
|
||||||
|
window.addEventListener('touchmove', onTouchMove, { passive: true });
|
||||||
|
window.addEventListener('touchend', onTouchEnd, { passive: true });
|
||||||
|
|
||||||
|
window.addEventListener('pagehide', () => {
|
||||||
|
window.removeEventListener('touchstart', onTouchStart);
|
||||||
|
window.removeEventListener('touchmove', onTouchMove);
|
||||||
|
window.removeEventListener('touchend', onTouchEnd);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
''';
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import 'package:core/utils/app_logger.dart';
|
import 'package:core/utils/app_logger.dart';
|
||||||
import 'package:core/utils/web_renderer/canvas_kit.dart';
|
import 'package:core/utils/web_renderer/canvas_kit.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:universal_html/html.dart' as universal_html;
|
||||||
|
|
||||||
abstract class PlatformInfo {
|
abstract class PlatformInfo {
|
||||||
@visibleForTesting
|
@visibleForTesting
|
||||||
@@ -44,4 +45,25 @@ abstract class PlatformInfo {
|
|||||||
log('PlatformInfo::platformNameOS(): $platformName');
|
log('PlatformInfo::platformNameOS(): $platformName');
|
||||||
return platformName;
|
return platformName;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static String get userAgent =>
|
||||||
|
isWeb ? universal_html.window.navigator.userAgent.toLowerCase() : '';
|
||||||
|
|
||||||
|
/// Web browser on a mobile phone
|
||||||
|
static bool get isWebMobile =>
|
||||||
|
isWeb && (userAgent.contains('iphone') ||
|
||||||
|
(userAgent.contains('android') && userAgent.contains('mobile')));
|
||||||
|
|
||||||
|
/// Web browser on a tablet device
|
||||||
|
static bool get isWebTablet =>
|
||||||
|
isWeb && (userAgent.contains('ipad') ||
|
||||||
|
(userAgent.contains('android') && !userAgent.contains('mobile')));
|
||||||
|
|
||||||
|
/// Web browser on a desktop (PC/Mac)
|
||||||
|
static bool get isWebDesktop =>
|
||||||
|
isWeb && !isWebMobile && !isWebTablet;
|
||||||
|
|
||||||
|
/// Touch devices
|
||||||
|
static bool get isWebTouchDevice =>
|
||||||
|
isWeb && (isWebMobile || isWebTablet);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user