TF-3644 Handle scroll thread detail when cursor is in email content area

Signed-off-by: dab246 <tdvu@linagora.com>
This commit is contained in:
dab246
2025-05-21 15:38:10 +07:00
committed by Dat H. Pham
parent 58a576adeb
commit 421fa321ba
3 changed files with 114 additions and 48 deletions
@@ -32,6 +32,7 @@ class HtmlContentViewerOnWeb extends StatefulWidget {
final bool allowResizeToDocumentSize;
final bool keepWidthWhileLoading;
final ScrollController? scrollController;
const HtmlContentViewerOnWeb({
Key? key,
@@ -45,6 +46,7 @@ class HtmlContentViewerOnWeb extends StatefulWidget {
this.onClickHyperLinkAction,
this.keepWidthWhileLoading = false,
this.contentPadding,
this.scrollController,
}) : super(key: key);
@override
@@ -65,10 +67,11 @@ class _HtmlContentViewerOnWebState extends State<HtmlContentViewerOnWeb> {
String? _htmlData;
bool _isLoading = true;
double minHeight = 100;
late final StreamSubscription<html.MessageEvent> sizeListener;
late final StreamSubscription<html.MessageEvent> _onMessageSubscription;
bool _iframeLoaded = false;
static const String iframeOnLoadMessage = 'iframeHasBeenLoaded';
static const String onClickHyperLinkName = 'onClickHyperLink';
static const String onScrollChangedEvent = 'onScrollChanged';
@override
void initState() {
@@ -77,64 +80,112 @@ class _HtmlContentViewerOnWebState extends State<HtmlContentViewerOnWeb> {
_actualWidth = widget.widthContent;
_createdViewId = _getRandString(10);
_setUpWeb();
_onMessageSubscription = html.window.onMessage.listen(_handleMessageEvent);
}
sizeListener = html.window.onMessage.listen((event) {
var data = json.decode(event.data);
void _handleMessageEvent(html.MessageEvent event) {
try {
final data = json.decode(event.data);
if (data['view'] != _createdViewId) return;
final viewId = data['view'];
if (viewId != _createdViewId) return;
final type = data['type'];
if (_isScrollChangedEventTriggered(type)) {
_handleIframeOnScrollChangedListener(data, widget.scrollController!);
return;
}
if (data['message'] == iframeOnLoadMessage) {
_iframeLoaded = true;
}
if (!_iframeLoaded) return;
if (data['type'] != null && data['type'].contains('toDart: htmlHeight')) {
final docHeight = data['height'] ?? _actualHeight;
if (docHeight != null && mounted) {
final scrollHeightWithBuffer = docHeight + 30.0;
if (scrollHeightWithBuffer > minHeight) {
setState(() {
_actualHeight = scrollHeightWithBuffer;
_isLoading = false;
});
}
}
if (mounted && _isLoading) {
setState(() {
_isLoading = false;
});
}
if (_isHtmlContentHeightEventTriggered(type)) {
_handleContentHeightEvent(data['height']);
} else if (_isHtmlContentWidthEventTriggered(type)) {
_handleContentWidthEvent(data['width']);
} else if (_isMailtoLinkEventTriggered(type)) {
_handleMailtoLinkEvent(data['url']);
} else if (_isHyperLinkEventTriggered(type)) {
_handleHyperLinkEvent(data['url']);
}
} catch (e) {
logError('_HtmlContentViewerOnWebState::_handleMessageEvent:Exception = $e');
}
}
if (data['type'] != null && data['type'].contains('toDart: htmlWidth') && !widget.keepWidthWhileLoading) {
final docWidth = data['width'] ?? _actualWidth;
if (docWidth != null && mounted) {
if (docWidth > _minWidth && widget.allowResizeToDocumentSize) {
setState(() {
_actualWidth = docWidth;
});
}
}
}
bool _isScrollChangedEventTriggered(String? type) {
return widget.scrollController != null &&
type?.contains('toDart: $onScrollChangedEvent') == true;
}
if (data['type'] != null && data['type'].contains('toDart: OpenLink')) {
final link = data['url'];
if (link != null && mounted) {
final urlString = link as String;
if (urlString.startsWith('mailto:')) {
widget.mailtoDelegate?.call(Uri.parse(urlString));
}
}
}
void _handleIframeOnScrollChangedListener(
dynamic data,
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);
} else {
controller.jumpTo(newOffset);
}
}
if (data['type'] != null && data['type'].contains('toDart: $onClickHyperLinkName')) {
final link = data['url'] as String?;
if (link != null && mounted) {
widget.onClickHyperLinkAction?.call(Uri.parse(link));
}
bool _isHtmlContentHeightEventTriggered(String? type) =>
type?.contains('toDart: htmlHeight') == true;
void _handleContentHeightEvent(dynamic height) {
final docHeight = height ?? _actualHeight;
if (docHeight != null && mounted) {
final scrollHeightWithBuffer = docHeight + 30.0;
if (scrollHeightWithBuffer > minHeight) {
setState(() {
_actualHeight = scrollHeightWithBuffer;
_isLoading = false;
});
}
});
}
if (mounted && _isLoading) {
setState(() {
_isLoading = false;
});
}
}
bool _isHtmlContentWidthEventTriggered(String? type) =>
type?.contains('toDart: htmlWidth') == true &&
!widget.keepWidthWhileLoading;
void _handleContentWidthEvent(dynamic width) {
final docWidth = width ?? _actualWidth;
if (docWidth != null && mounted &&
docWidth > _minWidth &&
widget.allowResizeToDocumentSize) {
setState(() => _actualWidth = docWidth);
}
}
bool _isMailtoLinkEventTriggered(String? type) =>
type?.contains('toDart: OpenLink') == true;
void _handleMailtoLinkEvent(dynamic url) {
if (url != null && mounted && url is String && url.startsWith('mailto:')) {
widget.mailtoDelegate?.call(Uri.tryParse(url));
}
}
bool _isHyperLinkEventTriggered(String? type) =>
type?.contains('toDart: $onClickHyperLinkName') == true;
void _handleHyperLinkEvent(dynamic url) {
if (url != null && mounted && url is String) {
widget.onClickHyperLinkAction?.call(Uri.tryParse(url));
}
}
@override
@@ -246,6 +297,17 @@ class _HtmlContentViewerOnWebState extends State<HtmlContentViewerOnWeb> {
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>
''';
@@ -362,7 +424,7 @@ class _HtmlContentViewerOnWebState extends State<HtmlContentViewerOnWeb> {
@override
void dispose() {
_htmlData = null;
sizeListener.cancel();
_onMessageSubscription.cancel();
super.dispose();
}
}
@@ -56,6 +56,7 @@ class EmailView extends GetWidget<SingleEmailController> {
this.isFirstEmailInThreadDetail = false,
this.threadSubject,
this.onToggleThreadDetailCollapseExpand,
this.scrollController,
});
final bool isInsideThreadDetailView;
@@ -63,7 +64,8 @@ class EmailView extends GetWidget<SingleEmailController> {
final bool isFirstEmailInThreadDetail;
final String? threadSubject;
final VoidCallback? onToggleThreadDetailCollapseExpand;
final ScrollController? scrollController;
@override
String? get tag => emailId?.id.value;
@@ -388,6 +390,7 @@ class EmailView extends GetWidget<SingleEmailController> {
direction: AppUtils.getCurrentDirection(context),
contentPadding: 0,
useDefaultFont: true,
scrollController: scrollController,
),
if (controller.mailboxDashBoardController.isDisplayedOverlayViewOnIFrame)
PointerInterceptor(
@@ -82,6 +82,7 @@ extension GetThreadDetailEmailViews on ThreadDetailController {
onToggleThreadDetailCollapseExpand: () {
toggleThreadDetailCollapeExpand(presentationEmail);
},
scrollController: scrollController,
),
);
}).toList();