-
Notifications
You must be signed in to change notification settings - Fork 79
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
TF-3267 Implement HTML attachment preview
- Loading branch information
1 parent
47b842d
commit 16d817c
Showing
12 changed files
with
271 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
class UnsupportedCharsetException implements Exception { | ||
const UnsupportedCharsetException(); | ||
} | ||
|
||
class NullCharsetException implements Exception { | ||
const NullCharsetException(); | ||
} |
92 changes: 92 additions & 0 deletions
92
core/lib/presentation/views/html_viewer/html_attachment_previewer.dart
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,92 @@ | ||
import 'dart:convert'; | ||
import 'dart:math'; | ||
|
||
import 'package:core/presentation/resources/image_paths.dart'; | ||
import 'package:core/presentation/utils/responsive_utils.dart'; | ||
import 'package:core/presentation/utils/shims/dart_ui.dart'; | ||
import 'package:core/presentation/views/button/tmail_button_widget.dart'; | ||
import 'package:core/presentation/views/responsive/responsive_widget.dart'; | ||
import 'package:flutter/material.dart'; | ||
import 'package:get/get.dart'; | ||
import 'package:pointer_interceptor/pointer_interceptor.dart'; | ||
import 'package:universal_html/html.dart'; | ||
|
||
class HtmlAttachmentPreviewer extends StatefulWidget { | ||
const HtmlAttachmentPreviewer({super.key, required this.htmlContent}); | ||
|
||
final String htmlContent; | ||
|
||
@override | ||
State<HtmlAttachmentPreviewer> createState() => _HtmlAttachmentPreviewerState(); | ||
} | ||
|
||
class _HtmlAttachmentPreviewerState extends State<HtmlAttachmentPreviewer> { | ||
late final IFrameElement _iframeElement; | ||
late final String _viewId; | ||
|
||
@override | ||
void initState() { | ||
super.initState(); | ||
_iframeElement = IFrameElement() | ||
..srcdoc = widget.htmlContent | ||
..style.border = 'none' | ||
..style.overflow = 'hidden' | ||
..style.width = '100%' | ||
..style.height = '100%'; | ||
_viewId = _getRandString(10); | ||
|
||
platformViewRegistry.registerViewFactory(_viewId, (int viewId) => _iframeElement); | ||
} | ||
|
||
|
||
@override | ||
Widget build(BuildContext context) { | ||
return Stack( | ||
children: [ | ||
Center( | ||
child: ResponsiveWidget( | ||
responsiveUtils: ResponsiveUtils(), | ||
desktop: Container( | ||
width: MediaQuery.sizeOf(context).width * 0.4, | ||
color: Colors.white, | ||
margin: const EdgeInsets.symmetric(vertical: 16), | ||
child: HtmlElementView(viewType: _viewId), | ||
), | ||
tablet: Container( | ||
width: MediaQuery.sizeOf(context).width * 0.8, | ||
color: Colors.white, | ||
margin: const EdgeInsets.symmetric(vertical: 16), | ||
child: HtmlElementView(viewType: _viewId), | ||
), | ||
mobile: Container( | ||
width: MediaQuery.sizeOf(context).width, | ||
color: Colors.white, | ||
margin: const EdgeInsets.symmetric(vertical: 16), | ||
child: HtmlElementView(viewType: _viewId), | ||
), | ||
), | ||
), | ||
Positioned( | ||
top: 16, | ||
right: 16, | ||
child: PointerInterceptor( | ||
child: TMailButtonWidget.fromIcon( | ||
icon: ImagePaths().icClose, | ||
iconSize: 40, | ||
onTapActionCallback: () { | ||
if (!mounted) return; | ||
Get.back(); | ||
}, | ||
), | ||
), | ||
), | ||
], | ||
); | ||
} | ||
|
||
String _getRandString(int len) { | ||
var random = Random.secure(); | ||
var values = List<int>.generate(len, (i) => random.nextInt(255)); | ||
return base64UrlEncode(values); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,10 +1,29 @@ | ||
import 'dart:convert'; | ||
import 'dart:typed_data'; | ||
|
||
import 'package:core/domain/exceptions/string_exception.dart'; | ||
|
||
class StringConvert { | ||
static String? writeEmptyToNull(String text) { | ||
static String? writeEmptyToNull(String text) { | ||
if (text.isEmpty) return null; | ||
return text; | ||
} | ||
|
||
static String writeNullToEmpty(String? text) { | ||
static String writeNullToEmpty(String? text) { | ||
return text ?? ''; | ||
} | ||
|
||
static String decodeFromBytes(Uint8List bytes, {required String? charset}) { | ||
if (charset == null) { | ||
throw const NullCharsetException(); | ||
} else if (charset.toLowerCase().contains('utf-8')) { | ||
return utf8.decode(bytes); | ||
} else if (charset.toLowerCase().contains('latin')) { | ||
return latin1.decode(bytes); | ||
} else if (charset.toLowerCase().contains('ascii')) { | ||
return ascii.decode(bytes); | ||
} else { | ||
throw const UnsupportedCharsetException(); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
17 changes: 17 additions & 0 deletions
17
lib/features/email/domain/state/get_html_content_from_attachment_state.dart
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
import 'package:core/presentation/state/failure.dart'; | ||
import 'package:core/presentation/state/success.dart'; | ||
|
||
class GettingHtmlContentFromAttachment extends LoadingState {} | ||
|
||
class GetHtmlContentFromAttachmentSuccess extends UIState { | ||
GetHtmlContentFromAttachmentSuccess({required this.sanitizedHtmlContent}); | ||
|
||
final String sanitizedHtmlContent; | ||
|
||
@override | ||
List<Object?> get props => [sanitizedHtmlContent]; | ||
} | ||
|
||
class GetHtmlContentFromAttachmentFailure extends FeatureFailure { | ||
GetHtmlContentFromAttachmentFailure({super.exception}); | ||
} |
82 changes: 82 additions & 0 deletions
82
lib/features/email/domain/usecases/get_html_content_from_attachment_interactor.dart
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,82 @@ | ||
import 'dart:async'; | ||
|
||
import 'package:core/presentation/state/failure.dart'; | ||
import 'package:core/presentation/state/success.dart'; | ||
import 'package:core/presentation/utils/html_transformer/transform_configuration.dart'; | ||
import 'package:core/utils/app_logger.dart'; | ||
import 'package:core/utils/string_convert.dart'; | ||
import 'package:dartz/dartz.dart'; | ||
import 'package:jmap_dart_client/jmap/account_id.dart'; | ||
import 'package:model/download/download_task_id.dart'; | ||
import 'package:model/email/attachment.dart'; | ||
import 'package:tmail_ui_user/features/email/domain/state/download_attachment_for_web_state.dart'; | ||
import 'package:tmail_ui_user/features/email/domain/state/get_html_content_from_attachment_state.dart'; | ||
import 'package:tmail_ui_user/features/email/domain/usecases/download_attachment_for_web_interactor.dart'; | ||
|
||
class GetHtmlContentFromAttachmentInteractor { | ||
GetHtmlContentFromAttachmentInteractor(this._downloadAttachmentForWebInteractor); | ||
|
||
final DownloadAttachmentForWebInteractor _downloadAttachmentForWebInteractor; | ||
|
||
Stream<Either<Failure, Success>> execute( | ||
AccountId accountId, | ||
Attachment attachment, | ||
DownloadTaskId taskId, | ||
String baseDownloadUrl, | ||
TransformConfiguration transformConfiguration, | ||
) async* { | ||
final onReceiveController = StreamController<Either<Failure, Success>>(); | ||
try { | ||
yield Right(GettingHtmlContentFromAttachment()); | ||
final downloadState = await _downloadAttachmentForWebInteractor.execute( | ||
taskId, | ||
attachment, | ||
accountId, | ||
baseDownloadUrl, | ||
onReceiveController, | ||
).last; | ||
|
||
Either<Failure, Success>? sanitizeState; | ||
await downloadState.fold( | ||
(failure) { | ||
sanitizeState = Left(GetHtmlContentFromAttachmentFailure( | ||
exception: failure is FeatureFailure ? failure.exception : null, | ||
)); | ||
}, | ||
(success) async { | ||
if (success is! DownloadAttachmentForWebSuccess) { | ||
sanitizeState = Right(GettingHtmlContentFromAttachment()); | ||
} else { | ||
final htmlContent = StringConvert.decodeFromBytes( | ||
success.bytes, | ||
charset: success.attachment.charset, | ||
); | ||
try { | ||
final sanitizedHtmlContent = await _downloadAttachmentForWebInteractor | ||
.emailRepository | ||
.sanitizeHtmlContent( | ||
htmlContent, | ||
transformConfiguration, | ||
); | ||
sanitizeState = Right(GetHtmlContentFromAttachmentSuccess( | ||
sanitizedHtmlContent: sanitizedHtmlContent, | ||
)); | ||
} catch (e) { | ||
sanitizeState = Left(GetHtmlContentFromAttachmentFailure(exception: e)); | ||
} | ||
} | ||
}, | ||
); | ||
|
||
onReceiveController.close(); | ||
if (sanitizeState != null) { | ||
yield sanitizeState!; | ||
} | ||
|
||
} catch (e) { | ||
logError('GetHtmlContentFromAttachmentInteractor:exception: $e'); | ||
onReceiveController.close(); | ||
yield Left(GetHtmlContentFromAttachmentFailure(exception: e)); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.