TF-4084 Fix cannot read or scroll emails when opening them in Twake Mail on Android chrome mobile browser

This commit is contained in:
dab246
2025-10-03 13:50:30 +07:00
committed by Dat H. Pham
parent d686b66f75
commit 0f3002879d
3 changed files with 170 additions and 22 deletions
@@ -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_template.dart';
import 'package:core/utils/html/html_utils.dart';
import 'package:core/utils/platform_info.dart';
import 'package:flutter/cupertino.dart';
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 onClickHyperLinkName = 'onClickHyperLink';
static const String onScrollChangedEvent = 'onScrollChanged';
static const String onScrollEndEvent = 'onScrollEnd';
@override
void initState() {
@@ -106,9 +108,12 @@ class _HtmlContentViewerOnWebState extends State<HtmlContentViewerOnWeb>
if (viewId != _createdViewId) return;
final type = data['type'];
if (_isScrollChangedEventTriggered(type)) {
if (_isScrollingIsAvailable && _isScrollChangedEventTriggered(type)) {
_handleIframeOnScrollChangedListener(data, widget.scrollController!);
return;
} else if (_isScrollingIsAvailable && _isScrollEndEventTriggered(type)) {
_handleIframeOnScrollEndListener(data, widget.scrollController!);
return;
}
if (data['message'] == iframeOnLoadMessage) {
@@ -130,10 +135,17 @@ class _HtmlContentViewerOnWebState extends State<HtmlContentViewerOnWeb>
}
}
bool _isScrollChangedEventTriggered(String? type) {
bool get _isScrollingIsAvailable {
return widget.scrollController != null &&
widget.scrollController?.hasClients == true &&
type?.contains('toDart: $onScrollChangedEvent') == true;
widget.scrollController?.hasClients == true;
}
bool _isScrollChangedEventTriggered(String? type) {
return type?.contains('toDart: $onScrollChangedEvent') == true;
}
bool _isScrollEndEventTriggered(String? type) {
return type?.contains('toDart: $onScrollEndEvent') == true;
}
void _handleIframeOnScrollChangedListener(
@@ -141,17 +153,51 @@ class _HtmlContentViewerOnWebState extends State<HtmlContentViewerOnWeb>
ScrollController controller,
) {
final deltaY = data['deltaY'] ?? 0.0;
final newOffset = controller.offset + deltaY;
log('_HtmlContentViewerOnWebState::_handleIframeOnScrollChangedListener:deltaY = $deltaY | newOffset = $newOffset');
if (newOffset < controller.position.minScrollExtent) {
controller.jumpTo(controller.position.minScrollExtent);
} else if (newOffset > controller.position.maxScrollExtent) {
controller.jumpTo(controller.position.maxScrollExtent);
final target = controller.offset + deltaY;
if (PlatformInfo.isWebTouchDevice) {
final newOffset = target.clamp(
controller.position.minScrollExtent,
controller.position.maxScrollExtent,
);
controller.animateTo(
newOffset,
duration: const Duration(milliseconds: 50),
curve: Curves.linear,
);
} 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) =>
type?.contains('toDart: htmlHeight') == true;
@@ -319,17 +365,6 @@ class _HtmlContentViewerOnWebState extends State<HtmlContentViewerOnWeb>
${!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>
''';
@@ -349,6 +384,17 @@ class _HtmlContentViewerOnWebState extends State<HtmlContentViewerOnWeb>
HtmlInteraction.scriptsHandleLazyLoadingBackgroundImage,
HtmlInteraction.generateNormalizeImageScript(widget.widthContent),
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();
final htmlTemplate = HtmlUtils.generateHtmlDocument(
+80
View File
@@ -298,4 +298,84 @@ class HtmlInteraction {
});
</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>
''';
}
+22
View File
@@ -1,6 +1,7 @@
import 'package:core/utils/app_logger.dart';
import 'package:core/utils/web_renderer/canvas_kit.dart';
import 'package:flutter/foundation.dart';
import 'package:universal_html/html.dart' as universal_html;
abstract class PlatformInfo {
@visibleForTesting
@@ -44,4 +45,25 @@ abstract class PlatformInfo {
log('PlatformInfo::platformNameOS(): $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);
}