TF-3601 Fix email view gone blank with large html content in iOS
Signed-off-by: dab246 <tdvu@linagora.com>
This commit is contained in:
@@ -19,6 +19,7 @@ typedef OnLoadWidthHtmlViewerAction = Function(bool isScrollPageViewActivated);
|
||||
typedef OnMailtoDelegateAction = Future<void> Function(Uri? uri);
|
||||
typedef OnPreviewEMLDelegateAction = Future<void> Function(Uri? uri);
|
||||
typedef OnDownloadAttachmentDelegateAction = Future<void> Function(Uri? uri);
|
||||
typedef OnHtmlContentClippedAction = Function(bool isClipped);
|
||||
|
||||
class HtmlContentViewer extends StatefulWidget {
|
||||
|
||||
@@ -34,6 +35,7 @@ class HtmlContentViewer extends StatefulWidget {
|
||||
final OnScrollHorizontalEndAction? onScrollHorizontalEnd;
|
||||
final OnPreviewEMLDelegateAction? onPreviewEMLDelegateAction;
|
||||
final OnDownloadAttachmentDelegateAction? onDownloadAttachmentDelegateAction;
|
||||
final OnHtmlContentClippedAction? onHtmlContentClippedAction;
|
||||
|
||||
const HtmlContentViewer({
|
||||
Key? key,
|
||||
@@ -48,6 +50,7 @@ class HtmlContentViewer extends StatefulWidget {
|
||||
this.onScrollHorizontalEnd,
|
||||
this.onPreviewEMLDelegateAction,
|
||||
this.onDownloadAttachmentDelegateAction,
|
||||
this.onHtmlContentClippedAction,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
@@ -57,6 +60,7 @@ class HtmlContentViewer extends StatefulWidget {
|
||||
class _HtmlContentViewState extends State<HtmlContentViewer> {
|
||||
|
||||
static const double _minHeight = 100.0;
|
||||
static const double _iOSHtmlContentMaxHeight = 22000.0;
|
||||
static const double _offsetHeight = 30.0;
|
||||
|
||||
late InAppWebViewController _webViewController;
|
||||
@@ -184,12 +188,25 @@ class _HtmlContentViewState extends State<HtmlContentViewer> {
|
||||
Size oldContentSize,
|
||||
Size newContentSize
|
||||
) async {
|
||||
if (!mounted || _loadingBarNotifier.value) return;
|
||||
|
||||
final maxContentHeight = math.max(oldContentSize.height, newContentSize.height);
|
||||
log('_HtmlContentViewState::_onContentSizeChanged:maxContentHeight: $maxContentHeight');
|
||||
if (maxContentHeight > _actualHeight && !_loadingBarNotifier.value && mounted) {
|
||||
log('_HtmlContentViewState::_onContentSizeChanged:HEIGHT_UPDATED: $maxContentHeight');
|
||||
if (maxContentHeight <= _actualHeight) return;
|
||||
|
||||
double currentHeight = maxContentHeight + _offsetHeight;
|
||||
|
||||
if (PlatformInfo.isIOS) {
|
||||
final isClipped = currentHeight > _iOSHtmlContentMaxHeight;
|
||||
if (isClipped) {
|
||||
widget.onHtmlContentClippedAction?.call(true);
|
||||
}
|
||||
currentHeight = currentHeight.clamp(_minHeight, _iOSHtmlContentMaxHeight);
|
||||
}
|
||||
|
||||
if (_actualHeight != currentHeight) {
|
||||
log('_HtmlContentViewState::_onContentSizeChanged: currentHeight = $currentHeight');
|
||||
setState(() {
|
||||
_actualHeight = maxContentHeight + _offsetHeight;
|
||||
_actualHeight = currentHeight;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -205,55 +222,84 @@ class _HtmlContentViewState extends State<HtmlContentViewer> {
|
||||
}
|
||||
|
||||
void _onHandleContentSizeChangedEvent(List<dynamic> parameters) async {
|
||||
final maxContentHeight = await _webViewController.evaluateJavascript(source: 'document.body.scrollHeight');
|
||||
log('_HtmlContentViewState::_onHandleContentSizeChangedEvent:maxContentHeight: $maxContentHeight');
|
||||
if (maxContentHeight is num && maxContentHeight > _actualHeight && !_loadingBarNotifier.value && mounted) {
|
||||
log('_HtmlContentViewState::_onHandleContentSizeChangedEvent:HEIGHT_UPDATED: $maxContentHeight');
|
||||
if (!mounted || _loadingBarNotifier.value) return;
|
||||
|
||||
final dynamic result = await _webViewController.evaluateJavascript(source: 'document.body.scrollHeight');
|
||||
if (result is! num) return;
|
||||
|
||||
final double maxContentHeight = result.toDouble();
|
||||
if (maxContentHeight <= _actualHeight) return;
|
||||
|
||||
double currentHeight = maxContentHeight + _offsetHeight;
|
||||
|
||||
if (PlatformInfo.isIOS) {
|
||||
final bool isClipped = currentHeight > _iOSHtmlContentMaxHeight;
|
||||
if (isClipped) {
|
||||
widget.onHtmlContentClippedAction?.call(true);
|
||||
}
|
||||
currentHeight = currentHeight.clamp(_minHeight, _iOSHtmlContentMaxHeight);
|
||||
}
|
||||
|
||||
if (_actualHeight != currentHeight) {
|
||||
log('_HtmlContentViewState::_onHandleContentSizeChangedEvent: currentHeight = $currentHeight');
|
||||
setState(() {
|
||||
_actualHeight = maxContentHeight + _offsetHeight;
|
||||
_actualHeight = currentHeight;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _getActualSizeHtmlViewer() async {
|
||||
final listSize = await Future.wait([
|
||||
_webViewController.evaluateJavascript(source: 'document.getElementsByClassName("tmail-content")[0].scrollWidth'),
|
||||
_webViewController.evaluateJavascript(source: 'document.getElementsByClassName("tmail-content")[0].offsetWidth'),
|
||||
_webViewController.evaluateJavascript(source: 'document.body.scrollHeight'),
|
||||
if (!mounted) return;
|
||||
|
||||
final List<dynamic> listSize = await Future.wait([
|
||||
_webViewController.evaluateJavascript(source: 'document.getElementsByClassName("tmail-content")[0]?.scrollWidth'),
|
||||
_webViewController.evaluateJavascript(source: 'document.getElementsByClassName("tmail-content")[0]?.offsetWidth'),
|
||||
_webViewController.evaluateJavascript(source: 'document.body?.scrollHeight'),
|
||||
]);
|
||||
log('_HtmlContentViewState::_getActualSizeHtmlViewer():listSize: $listSize');
|
||||
Set<Factory<OneSequenceGestureRecognizer>>? newGestureRecognizers;
|
||||
|
||||
log('_HtmlContentViewState::_getActualSizeHtmlViewer(): listSize: $listSize');
|
||||
|
||||
bool isScrollActivated = false;
|
||||
Set<Factory<OneSequenceGestureRecognizer>>? newGestureRecognizers;
|
||||
|
||||
if (listSize[0] is num && listSize[1] is num) {
|
||||
final scrollWidth = listSize[0] as num;
|
||||
final offsetWidth = listSize[1] as num;
|
||||
final double? scrollWidth = listSize[0] is num ? (listSize[0] as num).toDouble() : null;
|
||||
final double? offsetWidth = listSize[1] is num ? (listSize[1] as num).toDouble() : null;
|
||||
final double? scrollHeight = listSize[2] is num ? (listSize[2] as num).toDouble() : null;
|
||||
|
||||
if (scrollWidth != null && offsetWidth != null) {
|
||||
isScrollActivated = scrollWidth.round() == offsetWidth.round();
|
||||
|
||||
if (!isScrollActivated && PlatformInfo.isIOS) {
|
||||
newGestureRecognizers = {
|
||||
Factory<LongPressGestureRecognizer>(() => LongPressGestureRecognizer(duration: _longPressGestureDurationIOS)),
|
||||
Factory<HorizontalDragGestureRecognizer>(() => HorizontalDragGestureRecognizer())
|
||||
Factory<HorizontalDragGestureRecognizer>(() => HorizontalDragGestureRecognizer()),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (listSize[2] is num) {
|
||||
final scrollHeight = listSize[2] as num;
|
||||
if (mounted && scrollHeight > 0) {
|
||||
if (scrollHeight != null && scrollHeight > 0) {
|
||||
double currentHeight = scrollHeight + _offsetHeight;
|
||||
|
||||
if (PlatformInfo.isIOS) {
|
||||
final bool isClipped = currentHeight > _iOSHtmlContentMaxHeight;
|
||||
if (isClipped) {
|
||||
widget.onHtmlContentClippedAction?.call(true);
|
||||
}
|
||||
currentHeight = currentHeight.clamp(_minHeight, _iOSHtmlContentMaxHeight);
|
||||
}
|
||||
|
||||
if (_actualHeight != currentHeight || newGestureRecognizers != null) {
|
||||
log('_HtmlContentViewState::_getActualSizeHtmlViewer: currentHeight = $currentHeight');
|
||||
setState(() {
|
||||
_actualHeight = scrollHeight + _offsetHeight;
|
||||
_actualHeight = currentHeight;
|
||||
if (newGestureRecognizers != null) {
|
||||
_gestureRecognizers = newGestureRecognizers;
|
||||
}
|
||||
});
|
||||
}
|
||||
} else {
|
||||
if (mounted && newGestureRecognizers != null) {
|
||||
setState(() {
|
||||
_gestureRecognizers = newGestureRecognizers!;
|
||||
});
|
||||
}
|
||||
} else if (newGestureRecognizers != null) {
|
||||
setState(() {
|
||||
_gestureRecognizers = newGestureRecognizers!;
|
||||
});
|
||||
}
|
||||
|
||||
if (!isScrollActivated) {
|
||||
|
||||
@@ -190,6 +190,7 @@ class SingleEmailController extends BaseController with AppLoaderMixin {
|
||||
final attachmentsViewState = RxMap<Id, Either<Failure, Success>>();
|
||||
final isEmailContentHidden = RxBool(false);
|
||||
final currentEmailLoaded = Rxn<EmailLoaded>();
|
||||
final isEmailContentClipped = RxBool(false);
|
||||
|
||||
EmailId? _currentEmailId;
|
||||
Identity? _identitySelected;
|
||||
@@ -750,6 +751,7 @@ class SingleEmailController extends BaseController with AppLoaderMixin {
|
||||
blobCalendarEvent.value = null;
|
||||
emailUnsubscribe.value = null;
|
||||
_identitySelected = null;
|
||||
isEmailContentClipped.value = false;
|
||||
if (isEmailClosing) {
|
||||
emailLoadedViewState.value = Right(UIState.idle);
|
||||
viewState.value = Right(UIState.idle);
|
||||
@@ -2520,4 +2522,9 @@ class SingleEmailController extends BaseController with AppLoaderMixin {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
void onHtmlContentClippedAction(bool isClipped) {
|
||||
log('SingleEmailController::onHtmlContentClippedAction:isClipped = $isClipped');
|
||||
isEmailContentClipped.value = isClipped;
|
||||
}
|
||||
}
|
||||
@@ -390,7 +390,7 @@ class EmailView extends GetWidget<SingleEmailController> {
|
||||
else if (presentationEmail.id == controller.currentEmail?.id)
|
||||
Obx(() {
|
||||
if (controller.emailContents.value != null) {
|
||||
final allEmailContents = controller.emailContents.value ?? '';
|
||||
String allEmailContents = controller.emailContents.value ?? '';
|
||||
|
||||
if (PlatformInfo.isWeb) {
|
||||
return Expanded(
|
||||
@@ -429,29 +429,68 @@ class EmailView extends GetWidget<SingleEmailController> {
|
||||
}),
|
||||
),
|
||||
);
|
||||
} else if (PlatformInfo.isIOS
|
||||
&& !controller.responsiveUtils.isScreenWithShortestSide(context)) {
|
||||
} else if (PlatformInfo.isIOS) {
|
||||
return Obx(() {
|
||||
if (controller.isEmailContentHidden.isTrue) {
|
||||
if (controller.isEmailContentHidden.isTrue && !controller.responsiveUtils.isScreenWithShortestSide(context)) {
|
||||
return const SizedBox.shrink();
|
||||
} else {
|
||||
return Padding(
|
||||
padding: const EdgeInsetsDirectional.symmetric(
|
||||
vertical: EmailViewStyles.mobileContentVerticalMargin,
|
||||
horizontal: EmailViewStyles.mobileContentHorizontalMargin
|
||||
),
|
||||
child: LayoutBuilder(builder: (context, constraints) {
|
||||
return HtmlContentViewer(
|
||||
contentHtml: allEmailContents,
|
||||
initialWidth: constraints.maxWidth,
|
||||
direction: AppUtils.getCurrentDirection(context),
|
||||
contentPadding: 0,
|
||||
useDefaultFont: true,
|
||||
onMailtoDelegateAction: controller.openMailToLink,
|
||||
onScrollHorizontalEnd: controller.toggleScrollPhysicsPagerView,
|
||||
onLoadWidthHtmlViewer: controller.emailSupervisorController.updateScrollPhysicPageView,
|
||||
);
|
||||
})
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsetsDirectional.symmetric(
|
||||
vertical: EmailViewStyles.mobileContentVerticalMargin,
|
||||
horizontal: EmailViewStyles.mobileContentHorizontalMargin,
|
||||
),
|
||||
child: LayoutBuilder(builder: (context, constraints) {
|
||||
return HtmlContentViewer(
|
||||
contentHtml: allEmailContents,
|
||||
initialWidth: constraints.maxWidth,
|
||||
direction: AppUtils.getCurrentDirection(context),
|
||||
contentPadding: 0,
|
||||
useDefaultFont: true,
|
||||
onMailtoDelegateAction: controller.openMailToLink,
|
||||
onScrollHorizontalEnd: controller.toggleScrollPhysicsPagerView,
|
||||
onLoadWidthHtmlViewer: controller.emailSupervisorController.updateScrollPhysicPageView,
|
||||
onHtmlContentClippedAction: controller.onHtmlContentClippedAction,
|
||||
);
|
||||
}),
|
||||
),
|
||||
Obx(() {
|
||||
if (controller.isEmailContentClipped.isTrue) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Text(
|
||||
AppLocalizations.of(context).messageClipped,
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: AppColor.steelGray400,
|
||||
),
|
||||
),
|
||||
),
|
||||
TMailButtonWidget.fromText(
|
||||
text: AppLocalizations.of(context).viewEntireMessage.toUpperCase(),
|
||||
textStyle: Theme.of(context).textTheme.bodyLarge?.copyWith(
|
||||
color: AppColor.primaryColor,
|
||||
fontSize: 14,
|
||||
),
|
||||
margin: const EdgeInsetsDirectional.only(
|
||||
start: 8,
|
||||
end: 8,
|
||||
bottom: 24,
|
||||
),
|
||||
backgroundColor: Colors.transparent,
|
||||
onTapActionCallback: () {},
|
||||
),
|
||||
],
|
||||
);
|
||||
} else {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
}),
|
||||
],
|
||||
);
|
||||
}
|
||||
});
|
||||
@@ -471,6 +510,7 @@ class EmailView extends GetWidget<SingleEmailController> {
|
||||
onMailtoDelegateAction: controller.openMailToLink,
|
||||
onScrollHorizontalEnd: controller.toggleScrollPhysicsPagerView,
|
||||
onLoadWidthHtmlViewer: controller.emailSupervisorController.updateScrollPhysicPageView,
|
||||
onHtmlContentClippedAction: controller.onHtmlContentClippedAction,
|
||||
);
|
||||
})
|
||||
);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"@@last_modified": "2025-02-20T19:41:05.121752",
|
||||
"@@last_modified": "2025-03-31T03:40:41.454034",
|
||||
"initializing_data": "Initializing data...",
|
||||
"@initializing_data": {
|
||||
"type": "text",
|
||||
@@ -2962,6 +2962,12 @@
|
||||
"placeholders_order": [],
|
||||
"placeholders": {}
|
||||
},
|
||||
"errorWhileFetchingSubaddress": "Error while fetching the subaddress",
|
||||
"@errorWhileFetchingSubaddress": {
|
||||
"type": "text",
|
||||
"placeholders_order": [],
|
||||
"placeholders": {}
|
||||
},
|
||||
"connectedToTheInternet": "Connected to the internet",
|
||||
"@connectedToTheInternet": {
|
||||
"type": "text",
|
||||
@@ -4238,12 +4244,6 @@
|
||||
"placeholders_order": [],
|
||||
"placeholders": {}
|
||||
},
|
||||
"messageWarningDialogWhenExpiredOIDCTokenAndReconnection": "Your session expired. We need to take you back to the login page in order to refresh it. You might want to save the email you are currently editing before we do so.",
|
||||
"@messageWarningDialogWhenExpiredOIDCTokenAndReconnection": {
|
||||
"type": "text",
|
||||
"placeholders_order": [],
|
||||
"placeholders": {}
|
||||
},
|
||||
"replyToList": "Reply to list",
|
||||
"@replyToList": {
|
||||
"type": "text",
|
||||
@@ -4375,5 +4375,17 @@
|
||||
"type": "text",
|
||||
"placeholders_order": [],
|
||||
"placeholders": {}
|
||||
},
|
||||
"messageClipped": "[Message clipped]",
|
||||
"@messageClipped": {
|
||||
"type": "text",
|
||||
"placeholders_order": [],
|
||||
"placeholders": {}
|
||||
},
|
||||
"viewEntireMessage": "View entire message",
|
||||
"@viewEntireMessage": {
|
||||
"type": "text",
|
||||
"placeholders_order": [],
|
||||
"placeholders": {}
|
||||
}
|
||||
}
|
||||
@@ -4597,4 +4597,18 @@ class AppLocalizations {
|
||||
name: 'exitFullscreen',
|
||||
);
|
||||
}
|
||||
|
||||
String get messageClipped {
|
||||
return Intl.message(
|
||||
'[Message clipped]',
|
||||
name: 'messageClipped',
|
||||
);
|
||||
}
|
||||
|
||||
String get viewEntireMessage {
|
||||
return Intl.message(
|
||||
'View entire message',
|
||||
name: 'viewEntireMessage',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user