Fix long email content is cut off on android

Signed-off-by: dab246 <tdvu@linagora.com>
(cherry picked from commit cb53edf0eed467b40fefa85f7390dc3f5cfa6c35)
This commit is contained in:
dab246
2023-10-13 15:42:43 +07:00
committed by Dat H. Pham
parent 01ea3c4dbe
commit 72ba6d4145
4 changed files with 251 additions and 255 deletions
@@ -1,12 +1,9 @@
import 'dart:async'; import 'dart:async';
import 'dart:io'; import 'dart:math' as math;
import 'dart:math';
import 'package:core/presentation/extensions/color_extension.dart'; import 'package:core/core.dart';
import 'package:core/presentation/utils/html_transformer/html_event_action.dart'; import 'package:core/presentation/utils/html_transformer/html_event_action.dart';
import 'package:core/presentation/utils/html_transformer/html_template.dart';
import 'package:core/presentation/utils/html_transformer/html_utils.dart'; import 'package:core/presentation/utils/html_transformer/html_utils.dart';
import 'package:core/utils/app_logger.dart';
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart'; import 'package:flutter/gestures.dart';
@@ -14,38 +11,28 @@ import 'package:flutter_inappwebview/flutter_inappwebview.dart';
import 'package:url_launcher/url_launcher.dart' as launcher; import 'package:url_launcher/url_launcher.dart' as launcher;
import 'package:url_launcher/url_launcher_string.dart'; import 'package:url_launcher/url_launcher_string.dart';
typedef OnScrollHorizontalEnd = Function(bool leftDirection); typedef OnScrollHorizontalEndAction = Function(bool leftDirection);
typedef OnWebViewLoaded = Function(bool isScrollPageViewActivated); typedef OnLoadWidthHtmlViewerAction = Function(bool isScrollPageViewActivated);
typedef OnMailtoDelegateAction = Future<void> Function(Uri? uri);
class HtmlContentViewer extends StatefulWidget { class HtmlContentViewer extends StatefulWidget {
final String contentHtml; final String contentHtml;
final double? heightContent; final double? initialWidth;
final OnScrollHorizontalEnd? onScrollHorizontalEnd;
final OnWebViewLoaded? onWebViewLoaded;
final TextDirection? direction; final TextDirection? direction;
/// Register this callback if you want a reference to the [InAppWebViewController]. final OnLoadWidthHtmlViewerAction? onLoadWidthHtmlViewer;
final void Function(InAppWebViewController controller)? onCreated; final OnMailtoDelegateAction? onMailtoDelegateAction;
final OnScrollHorizontalEndAction? onScrollHorizontalEnd;
/// Handler for mailto: links
final Future Function(Uri mailto)? mailtoDelegate;
/// Handler for any non-media URLs that the user taps on the website.
///
/// Returns `true` when the given `url` was handled.
final Future<bool> Function(Uri url)? urlLauncherDelegate;
const HtmlContentViewer({ const HtmlContentViewer({
Key? key, Key? key,
required this.contentHtml, required this.contentHtml,
this.heightContent, this.initialWidth,
this.onCreated,
this.onWebViewLoaded,
this.onScrollHorizontalEnd,
this.urlLauncherDelegate,
this.mailtoDelegate,
this.direction, this.direction,
this.onLoadWidthHtmlViewer,
this.onMailtoDelegateAction,
this.onScrollHorizontalEnd
}) : super(key: key); }) : super(key: key);
@override @override
@@ -57,24 +44,39 @@ class _HtmlContentViewState extends State<HtmlContentViewer> {
static const double _minHeight = 100.0; static const double _minHeight = 100.0;
static const double _offsetHeight = 30.0; static const double _offsetHeight = 30.0;
late double _actualHeight;
String? _htmlData;
late InAppWebViewController _webViewController; late InAppWebViewController _webViewController;
bool _isLoading = true; late double _actualHeight;
bool _horizontalGestureActivated = false; late Set<Factory<OneSequenceGestureRecognizer>> _gestureRecognizers;
final _loadingBarNotifier = ValueNotifier(true);
String? _htmlData;
final _webViewSetting = InAppWebViewSettings(
transparentBackground: true,
verticalScrollBarEnabled: false,
);
@override @override
void initState() { void initState() {
super.initState(); super.initState();
if (Platform.isAndroid) {
_actualHeight = widget.heightContent ?? _minHeight;
} else {
_actualHeight = _minHeight; _actualHeight = _minHeight;
if (PlatformInfo.isAndroid) {
_gestureRecognizers = {
Factory<LongPressGestureRecognizer>(() => LongPressGestureRecognizer()),
Factory<ScaleGestureRecognizer>(() => ScaleGestureRecognizer()),
};
} else {
_gestureRecognizers = {
Factory<LongPressGestureRecognizer>(() => LongPressGestureRecognizer()),
};
} }
log('_HtmlContentViewState::initState():_actualHeight: $_actualHeight');
_htmlData = generateHtml( _htmlData = generateHtml(
widget.contentHtml, widget.contentHtml,
direction: widget.direction, direction: widget.direction,
javaScripts: PlatformInfo.isAndroid
? HtmlUtils.scriptsHandleContentSizeChanged
: null
); );
} }
@@ -84,76 +86,73 @@ class _HtmlContentViewState extends State<HtmlContentViewer> {
log('_HtmlContentViewState::didUpdateWidget():Old-Direction: ${oldWidget.direction} | Current-Direction: ${widget.direction}'); log('_HtmlContentViewState::didUpdateWidget():Old-Direction: ${oldWidget.direction} | Current-Direction: ${widget.direction}');
if (widget.contentHtml != oldWidget.contentHtml || if (widget.contentHtml != oldWidget.contentHtml ||
widget.direction != oldWidget.direction) { widget.direction != oldWidget.direction) {
_actualHeight = _minHeight;
_htmlData = generateHtml( _htmlData = generateHtml(
widget.contentHtml, widget.contentHtml,
direction: widget.direction, direction: widget.direction,
javaScripts: PlatformInfo.isAndroid
? HtmlUtils.scriptsHandleContentSizeChanged
: null
); );
} }
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return LayoutBuilder(builder: (context, constraints) { return Stack(children: [
return Stack( if (_htmlData == null)
children: [
if (_htmlData == null || _htmlData?.isEmpty == true)
const SizedBox.shrink() const SizedBox.shrink()
else else
SizedBox( SizedBox(
height: _actualHeight, height: _actualHeight,
width: constraints.maxWidth, width: widget.initialWidth,
child: InAppWebView( child: InAppWebView(
key: ValueKey(_htmlData), key: ValueKey(_htmlData),
initialSettings: InAppWebViewSettings( initialSettings: _webViewSetting,
transparentBackground: true, onWebViewCreated: _onWebViewCreated,
verticalScrollBarEnabled: false
),
onWebViewCreated: (controller) async {
_webViewController = controller;
await controller.loadData(data: _htmlData ?? '');
widget.onCreated?.call(controller);
},
onLoadStop: _onLoadStop, onLoadStop: _onLoadStop,
onContentSizeChanged: _onContentSizeChanged, onContentSizeChanged: _onContentSizeChanged,
shouldOverrideUrlLoading: _shouldOverrideUrlLoading, shouldOverrideUrlLoading: _shouldOverrideUrlLoading,
gestureRecognizers: { gestureRecognizers: _gestureRecognizers,
Factory<LongPressGestureRecognizer>(() => LongPressGestureRecognizer()),
if (Platform.isIOS && _horizontalGestureActivated)
Factory<HorizontalDragGestureRecognizer>(() => HorizontalDragGestureRecognizer()),
if (Platform.isAndroid)
Factory<ScaleGestureRecognizer>(() => ScaleGestureRecognizer()),
},
onScrollChanged: (controller, x, y) => controller.scrollTo(x: 0, y: 0) onScrollChanged: (controller, x, y) => controller.scrollTo(x: 0, y: 0)
)
), ),
ValueListenableBuilder(
valueListenable: _loadingBarNotifier,
builder: (context, loading, child) {
if (loading) {
return const CupertinoLoadingWidget(isCenter: false);
} else {
return const SizedBox.shrink();
}
}
), ),
if (_isLoading) ]);
const Align(
alignment: Alignment.topCenter,
child: SizedBox(
width: 30,
height: 30,
child: CupertinoActivityIndicator(
color: AppColor.colorLoading
)
)
)
],
);
});
} }
void _onLoadStop(InAppWebViewController controller, WebUri? webUri) async { void _onWebViewCreated(InAppWebViewController controller) async {
await Future.wait([ log('_HtmlContentViewState::_onWebViewCreated:');
_setActualHeightView(), _webViewController = controller;
_setActualWidthView(),
]);
_hideLoadingProgress(); await controller.loadData(data: _htmlData ?? '');
controller.addJavaScriptHandler( controller.addJavaScriptHandler(
handlerName: HtmlUtils.scrollEventJSChannelName, handlerName: HtmlUtils.scrollEventJSChannelName,
callback: _onHandleScrollEvent callback: _onHandleScrollEvent
); );
if (PlatformInfo.isAndroid) {
controller.addJavaScriptHandler(
handlerName: HtmlUtils.contentSizeChangedEventJSChannelName,
callback: _onHandleContentSizeChangedEvent
);
}
}
void _onLoadStop(InAppWebViewController controller, WebUri? webUri) async {
log('_HtmlContentViewState::_onLoadStop:');
await _getActualSizeHtmlViewer();
_loadingBarNotifier.value = false;
} }
void _onContentSizeChanged( void _onContentSizeChanged(
@@ -161,12 +160,12 @@ class _HtmlContentViewState extends State<HtmlContentViewer> {
Size oldContentSize, Size oldContentSize,
Size newContentSize Size newContentSize
) async { ) async {
log('_HtmlContentViewState::_onContentSizeChanged:oldContentSize: $oldContentSize | newContentSize: $newContentSize'); final maxContentHeight = math.max(oldContentSize.height, newContentSize.height);
final maxContentHeight = max(oldContentSize.height, newContentSize.height); log('_HtmlContentViewState::_onContentSizeChanged:maxContentHeight: $maxContentHeight');
if (!_isLoading && maxContentHeight > _actualHeight) { if (maxContentHeight > _actualHeight && !_loadingBarNotifier.value && mounted) {
log('_HtmlContentViewState::_onContentSizeChanged:HEIGHT_UPDATED: $maxContentHeight'); log('_HtmlContentViewState::_onContentSizeChanged:HEIGHT_UPDATED: $maxContentHeight');
setState(() { setState(() {
_actualHeight = maxContentHeight; _actualHeight = maxContentHeight + _offsetHeight;
}); });
} }
} }
@@ -181,64 +180,62 @@ class _HtmlContentViewState extends State<HtmlContentViewer> {
} }
} }
Future<void> _setActualHeightView() async { void _onHandleContentSizeChangedEvent(List<dynamic> parameters) async {
if (Platform.isAndroid) { final maxContentHeight = await _webViewController.evaluateJavascript(source: 'document.body.scrollHeight');
await Future.delayed(const Duration(milliseconds: 1000)); log('_HtmlContentViewState::_onHandleContentSizeChangedEvent:maxContentHeight: $maxContentHeight');
} if (maxContentHeight is num && maxContentHeight > _actualHeight && !_loadingBarNotifier.value && mounted) {
final scrollHeight = await _webViewController.getContentHeight(); log('_HtmlContentViewState::_onHandleContentSizeChangedEvent:HEIGHT_UPDATED: $maxContentHeight');
log('_HtmlContentViewState::_setActualHeightView():scrollHeight: $scrollHeight');
if (mounted &&
scrollHeight != null &&
scrollHeight > 0
) {
final newHeight = scrollHeight + _offsetHeight;
log('_HtmlContentViewState::_setActualHeightView():newHeight: $newHeight');
setState(() { setState(() {
_actualHeight = newHeight; _actualHeight = maxContentHeight + _offsetHeight;
_isLoading = false;
}); });
} }
} }
Future<void> _setActualWidthView() async { Future<void> _getActualSizeHtmlViewer() async {
final result = await Future.wait([ final listSize = await Future.wait([
_webViewController.evaluateJavascript(source: 'document.getElementsByClassName("tmail-content")[0].scrollWidth'), _webViewController.evaluateJavascript(source: 'document.getElementsByClassName("tmail-content")[0].scrollWidth'),
_webViewController.evaluateJavascript(source: 'document.getElementsByClassName("tmail-content")[0].offsetWidth') _webViewController.evaluateJavascript(source: 'document.getElementsByClassName("tmail-content")[0].offsetWidth'),
_webViewController.evaluateJavascript(source: 'document.body.scrollHeight'),
]); ]);
log('_HtmlContentViewState::_setActualWidthView():result: $result'); log('_HtmlContentViewState::_getActualSizeHtmlViewer():listSize: $listSize');
if (result.length == 2) { Set<Factory<OneSequenceGestureRecognizer>>? newGestureRecognizers;
final scrollWidth = result[0]; bool isScrollActivated = false;
final offsetWidth = result[1];
if (mounted &&
scrollWidth != null &&
offsetWidth != null &&
scrollWidth is num &&
offsetWidth is num
) {
final isScrollActivated = scrollWidth.round() == offsetWidth.round();
if (isScrollActivated) {
setState(() {
_horizontalGestureActivated = false;
});
} else {
setState(() {
_horizontalGestureActivated = true;
});
if (listSize[0] is num && listSize[1] is num) {
final scrollWidth = listSize[0] as num;
final offsetWidth = listSize[1] as num;
isScrollActivated = scrollWidth.round() == offsetWidth.round();
if (!isScrollActivated && PlatformInfo.isIOS) {
newGestureRecognizers = {
Factory<LongPressGestureRecognizer>(() => LongPressGestureRecognizer()),
Factory<HorizontalDragGestureRecognizer>(() => HorizontalDragGestureRecognizer())
};
}
}
if (listSize[2] is num) {
final scrollHeight = listSize[2] as num;
if (mounted && scrollHeight > 0) {
setState(() {
_actualHeight = scrollHeight + _offsetHeight;
if (newGestureRecognizers != null) {
_gestureRecognizers = newGestureRecognizers;
}
});
}
} else {
if (mounted && newGestureRecognizers != null) {
setState(() {
_gestureRecognizers = newGestureRecognizers!;
});
}
}
if (!isScrollActivated) {
await _webViewController.evaluateJavascript(source: HtmlUtils.runScriptsHandleScrollEvent); await _webViewController.evaluateJavascript(source: HtmlUtils.runScriptsHandleScrollEvent);
} }
widget.onLoadWidthHtmlViewer?.call(isScrollActivated);
widget.onWebViewLoaded?.call(isScrollActivated);
}
}
}
void _hideLoadingProgress() {
if (mounted && _isLoading) {
setState(() {
_isLoading = false;
});
}
} }
Future<NavigationActionPolicy?> _shouldOverrideUrlLoading( Future<NavigationActionPolicy?> _shouldOverrideUrlLoading(
@@ -256,17 +253,12 @@ class _HtmlContentViewState extends State<HtmlContentViewer> {
} }
final requestUri = Uri.parse(url); final requestUri = Uri.parse(url);
final mailtoHandler = widget.mailtoDelegate; final mailtoHandler = widget.onMailtoDelegateAction;
if (mailtoHandler != null && requestUri.isScheme('mailto')) { if (mailtoHandler != null && requestUri.isScheme('mailto')) {
await mailtoHandler(requestUri); await mailtoHandler(requestUri);
return NavigationActionPolicy.CANCEL; return NavigationActionPolicy.CANCEL;
} }
final urlDelegate = widget.urlLauncherDelegate;
if (urlDelegate != null) {
await urlDelegate(Uri.parse(url));
return NavigationActionPolicy.CANCEL;
}
if (await launcher.canLaunchUrl(Uri.parse(url))) { if (await launcher.canLaunchUrl(Uri.parse(url))) {
await launcher.launchUrl( await launcher.launchUrl(
Uri.parse(url), Uri.parse(url),
@@ -276,4 +268,10 @@ class _HtmlContentViewState extends State<HtmlContentViewer> {
return NavigationActionPolicy.CANCEL; return NavigationActionPolicy.CANCEL;
} }
@override
void dispose() {
_loadingBarNotifier.dispose();
super.dispose();
}
} }
@@ -1,5 +1,6 @@
import 'dart:collection'; import 'dart:collection';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:core/utils/app_logger.dart';
import 'package:core/utils/platform_info.dart'; import 'package:core/utils/platform_info.dart';
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@@ -34,7 +35,7 @@ class EmailSupervisorController extends GetxController {
@override @override
void onInit() { void onInit() {
super.onInit(); super.onInit();
updateScrollPhysicPageView(); updateScrollPhysicPageView(false);
} }
@override @override
@@ -69,7 +70,7 @@ class EmailSupervisorController extends GetxController {
} }
void onPageChanged(int index) { void onPageChanged(int index) {
updateScrollPhysicPageView(); updateScrollPhysicPageView(false);
mailboxDashBoardController.openEmailDetailedView(currentListEmail[index]); mailboxDashBoardController.openEmailDetailedView(currentListEmail[index]);
} }
@@ -122,7 +123,8 @@ class EmailSupervisorController extends GetxController {
} }
} }
void updateScrollPhysicPageView({bool isScrollPageViewActivated = false}) { void updateScrollPhysicPageView(bool isScrollPageViewActivated) {
log('EmailSupervisorController::updateScrollPhysicPageView:isScrollPageViewActivated: $isScrollPageViewActivated');
if (PlatformInfo.isWeb || !isScrollPageViewActivated) { if (PlatformInfo.isWeb || !isScrollPageViewActivated) {
scrollPhysicsPageView.value = const NeverScrollableScrollPhysics(); scrollPhysicsPageView.value = const NeverScrollableScrollPhysics();
} else { } else {
@@ -121,8 +121,7 @@ class EmailView extends GetWidget<SingleEmailController> {
} }
}), }),
Expanded( Expanded(
child: LayoutBuilder(builder: (context, constraints) { child: Obx(() {
return Obx(() {
if (controller.emailSupervisorController.supportedPageView.isTrue) { if (controller.emailSupervisorController.supportedPageView.isTrue) {
final currentListEmail = controller.emailSupervisorController.currentListEmail; final currentListEmail = controller.emailSupervisorController.currentListEmail;
return PageView.builder( return PageView.builder(
@@ -144,7 +143,6 @@ class EmailView extends GetWidget<SingleEmailController> {
context: context, context: context,
presentationEmail: currentEmail, presentationEmail: currentEmail,
calendarEvent: controller.calendarEvent.value, calendarEvent: controller.calendarEvent.value,
maxHeight: constraints.maxHeight
)) ))
) )
); );
@@ -188,7 +186,6 @@ class EmailView extends GetWidget<SingleEmailController> {
context: context, context: context,
presentationEmail: currentEmail, presentationEmail: currentEmail,
calendarEvent: controller.calendarEvent.value, calendarEvent: controller.calendarEvent.value,
maxHeight: constraints.maxHeight
)) ))
) )
); );
@@ -219,7 +216,6 @@ class EmailView extends GetWidget<SingleEmailController> {
}); });
} }
} }
});
}), }),
), ),
EmailViewBottomBarWidget( EmailViewBottomBarWidget(
@@ -306,7 +302,6 @@ class EmailView extends GetWidget<SingleEmailController> {
required PresentationEmail presentationEmail, required PresentationEmail presentationEmail,
CalendarEvent? calendarEvent, CalendarEvent? calendarEvent,
List<String>? emailAddressSender, List<String>? emailAddressSender,
double? maxHeight,
}) { }) {
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
@@ -410,17 +405,16 @@ class EmailView extends GetWidget<SingleEmailController> {
vertical: EmailViewStyles.mobileContentVerticalMargin, vertical: EmailViewStyles.mobileContentVerticalMargin,
horizontal: EmailViewStyles.mobileContentHorizontalMargin horizontal: EmailViewStyles.mobileContentHorizontalMargin
), ),
child: HtmlContentViewer( child: LayoutBuilder(builder: (context, constraints) {
return HtmlContentViewer(
contentHtml: allEmailContents, contentHtml: allEmailContents,
heightContent: maxHeight, initialWidth: constraints.maxWidth,
mailtoDelegate: controller.openMailToLink,
onScrollHorizontalEnd: controller.toggleScrollPhysicsPagerView,
onWebViewLoaded: (isScrollPageViewActivated) {
log('EmailView::_buildEmailContent(): isScrollPageViewActivated: $isScrollPageViewActivated');
controller.emailSupervisorController.updateScrollPhysicPageView(isScrollPageViewActivated: isScrollPageViewActivated);
},
direction: AppUtils.getCurrentDirection(context), direction: AppUtils.getCurrentDirection(context),
), onMailtoDelegateAction: controller.openMailToLink,
onScrollHorizontalEnd: controller.toggleScrollPhysicsPagerView,
onLoadWidthHtmlViewer: controller.emailSupervisorController.updateScrollPhysicPageView,
);
})
); );
} }
} else { } else {
@@ -46,11 +46,13 @@ class SignatureBuilder extends StatelessWidget {
direction: AppUtils.getCurrentDirection(context), direction: AppUtils.getCurrentDirection(context),
); );
} else { } else {
return LayoutBuilder(builder: (context, constraints) {
return HtmlContentViewer( return HtmlContentViewer(
contentHtml: signatureSelected, contentHtml: signatureSelected,
heightContent: height, initialWidth: constraints.maxWidth,
direction: AppUtils.getCurrentDirection(context), direction: AppUtils.getCurrentDirection(context),
); );
});
} }
} else { } else {
return SizedBox(width: width, height: height); return SizedBox(width: width, height: height);