Skip to content

Commit

Permalink
fixup! TF-3267 Implement HTML attachment preview
Browse files Browse the repository at this point in the history
  • Loading branch information
tddang-linagora committed Dec 10, 2024
1 parent 32e7f9e commit 56fb1a9
Show file tree
Hide file tree
Showing 5 changed files with 188 additions and 73 deletions.
94 changes: 94 additions & 0 deletions core/test/utils/string_convert_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import 'dart:convert';

import 'package:core/domain/exceptions/string_exception.dart';
import 'package:core/utils/string_convert.dart';
import 'package:flutter_test/flutter_test.dart';

void main() {
group('string convert test:', () {
const testText = 'Hello';
test(
'should use utf8 decoder '
'when isHtml is true',
() {
// arrange
final bytes = utf8.encode(testText);

// act
final result = StringConvert.decodeFromBytes(bytes, charset: null, isHtml: true);

// assert
expect(result, testText);
});

test(
'should use utf8 decoder '
'when charset contains utf-8',
() {
// arrange
final bytes = utf8.encode(testText);

// act
final result = StringConvert.decodeFromBytes(bytes, charset: 'utf-8');

// assert
expect(result, testText);
});

test(
'should use latin1 decoder '
'when charset contains latin-1',
() {
// arrange
final bytes = latin1.encode(testText);

// act
final result = StringConvert.decodeFromBytes(bytes, charset: 'latin-1');

// assert
expect(result, testText);
});

test(
'should use ascii decoder '
'when charset contains ascii',
() {
// arrange
final bytes = ascii.encode(testText);

// act
final result = StringConvert.decodeFromBytes(bytes, charset: 'ascii');

// assert
expect(result, testText);
});

test(
'should throw NullCharsetException '
'when charset is null',
() {
// arrange
final bytes = utf8.encode(testText);

// assert
expect(
() => StringConvert.decodeFromBytes(bytes, charset: null),
throwsA(isA<NullCharsetException>()),
);
});

test(
'should throw UnsupportedCharsetException '
'when charset is unsupported',
() {
// arrange
final bytes = utf8.encode(testText);

// assert
expect(
() => StringConvert.decodeFromBytes(bytes, charset: 'unsupported'),
throwsA(isA<UnsupportedCharsetException>()),
);
});
});
}
Original file line number Diff line number Diff line change
@@ -1,19 +1,26 @@
import 'package:core/presentation/state/failure.dart';
import 'package:core/presentation/state/success.dart';
import 'package:model/email/attachment.dart';

class GettingHtmlContentFromAttachment extends LoadingState {}

class GetHtmlContentFromAttachmentSuccess extends UIState {
GetHtmlContentFromAttachmentSuccess({
required this.sanitizedHtmlContent,
required this.htmlAttachmentTitle,
required this.attachment,
});

final String sanitizedHtmlContent;
final String htmlAttachmentTitle;
final Attachment attachment;

@override
List<Object?> get props => [sanitizedHtmlContent, htmlAttachmentTitle];
List<Object?> get props => [
sanitizedHtmlContent,
htmlAttachmentTitle,
attachment,
];
}

class GetHtmlContentFromAttachmentFailure extends FeatureFailure {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,20 +52,11 @@ class GetHtmlContentFromAttachmentInteractor {
charset: success.attachment.charset,
isHtml: true,
);
try {
final sanitizedHtmlContent = await _downloadAttachmentForWebInteractor
.emailRepository
.sanitizeHtmlContent(
htmlContent,
transformConfiguration,
);
sanitizeState = Right(GetHtmlContentFromAttachmentSuccess(
sanitizedHtmlContent: sanitizedHtmlContent,
htmlAttachmentTitle: attachment.generateFileName(),
));
} catch (e) {
sanitizeState = Left(GetHtmlContentFromAttachmentFailure(exception: e));
}
sanitizeState = await _sanitizeHtmlContent(
htmlContent,
transformConfiguration,
attachment,
);
}
},
);
Expand All @@ -83,4 +74,26 @@ class GetHtmlContentFromAttachmentInteractor {
yield Left(GetHtmlContentFromAttachmentFailure(exception: e));
}
}

Future<Either<Failure, Success>?> _sanitizeHtmlContent(
String htmlContent,
TransformConfiguration transformConfiguration,
Attachment attachment,
) async {
try {
final sanitizedHtmlContent = await _downloadAttachmentForWebInteractor
.emailRepository
.sanitizeHtmlContent(
htmlContent,
transformConfiguration,
);
return Right(GetHtmlContentFromAttachmentSuccess(
sanitizedHtmlContent: sanitizedHtmlContent,
htmlAttachmentTitle: attachment.generateFileName(),
attachment: attachment,
));
} catch (e) {
return Left(GetHtmlContentFromAttachmentFailure(exception: e));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,10 @@ class SingleEmailController extends BaseController with AppLoaderMixin {
Get.dialog(HtmlAttachmentPreviewer(
title: success.htmlAttachmentTitle,
htmlContent: success.sanitizedHtmlContent,
mailToClicked: openMailToLink,
downloadAttachmentClicked: () {
downloadAttachmentForWeb(success.attachment);
},
));
}
}
Expand Down
113 changes: 55 additions & 58 deletions lib/features/email/presentation/widgets/html_attachment_previewer.dart
Original file line number Diff line number Diff line change
@@ -1,90 +1,87 @@
import 'dart:convert';
import 'dart:math';

import 'package:core/presentation/utils/responsive_utils.dart';
import 'package:core/presentation/utils/shims/dart_ui.dart';
import 'package:core/presentation/views/html_viewer/html_content_viewer_on_web_widget.dart';
import 'package:core/presentation/views/responsive/responsive_widget.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:tmail_ui_user/features/email/presentation/widgets/pdf_viewer/top_bar_attachment_viewer.dart';
import 'package:universal_html/html.dart';
import 'package:tmail_ui_user/main/utils/app_utils.dart';

class HtmlAttachmentPreviewer extends StatefulWidget {
class HtmlAttachmentPreviewer extends StatelessWidget {
const HtmlAttachmentPreviewer({
super.key,
required this.htmlContent,
required this.title,
required this.mailToClicked,
required this.downloadAttachmentClicked,
});

final String title;
final String htmlContent;
final void Function(Uri? mailToUri) mailToClicked;
final VoidCallback downloadAttachmentClicked;

@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);
}

static const double _verticalMargin = 16;

@override
Widget build(BuildContext context) {
return Column(
children: [
TopBarAttachmentViewer(
title: widget.title,
closeAction: () {
if (!mounted) return;
Get.back();
},
title: title,
closeAction: Get.back,
downloadAction: downloadAttachmentClicked,
),
Expanded(
child: 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),
),
),
child: LayoutBuilder(
builder: (context, constraints) {
return SingleChildScrollView(
child: Center(
child: Container(
margin: const EdgeInsets.symmetric(vertical: _verticalMargin),
color: Colors.white,
child: ResponsiveWidget(
responsiveUtils: ResponsiveUtils(),
desktop: _buildHtmlViewerWith(
context,
width: constraints.maxWidth * 0.4,
height: constraints.maxHeight - _verticalMargin * 2
),
tablet: _buildHtmlViewerWith(
context,
width: constraints.maxWidth * 0.8,
height: constraints.maxHeight - _verticalMargin * 2
),
mobile: _buildHtmlViewerWith(
context,
width: constraints.maxWidth,
height: constraints.maxHeight - _verticalMargin * 2
),
),
),
),
);
}
),
),
],
);
}

String _getRandString(int len) {
var random = Random.secure();
var values = List<int>.generate(len, (i) => random.nextInt(255));
return base64UrlEncode(values);
HtmlContentViewerOnWeb _buildHtmlViewerWith(
BuildContext context, {
required double width,
required double height,
}) {
return HtmlContentViewerOnWeb(
contentHtml: htmlContent,
widthContent: width,
heightContent: height,
direction: AppUtils.getCurrentDirection(context),
mailtoDelegate: (uri) {
Get.back();
mailToClicked(uri);
},
);
}
}

0 comments on commit 56fb1a9

Please sign in to comment.