From 5308a074b429b3904503ebab228f358999b22b49 Mon Sep 17 00:00:00 2001 From: krille-chan Date: Sat, 9 Mar 2024 16:36:09 +0100 Subject: [PATCH] refactor: New html formatting --- lib/pages/chat/events/html_message.dart | 414 +++--------------------- pubspec.lock | 172 +++++++--- pubspec.yaml | 3 +- 3 files changed, 177 insertions(+), 412 deletions(-) diff --git a/lib/pages/chat/events/html_message.dart b/lib/pages/chat/events/html_message.dart index 8726bb7f6..e72d3a423 100644 --- a/lib/pages/chat/events/html_message.dart +++ b/lib/pages/chat/events/html_message.dart @@ -1,13 +1,14 @@ +import 'package:adaptive_dialog/adaptive_dialog.dart'; import 'package:flutter/material.dart'; import 'package:collection/collection.dart'; import 'package:flutter_highlighter/flutter_highlighter.dart'; import 'package:flutter_highlighter/themes/shades-of-purple.dart'; -import 'package:flutter_html/flutter_html.dart'; -import 'package:flutter_html_table/flutter_html_table.dart'; import 'package:flutter_math_fork/flutter_math.dart'; +import 'package:flutter_widget_from_html/flutter_widget_from_html.dart'; import 'package:html/dom.dart' as dom; import 'package:linkify/linkify.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:matrix/matrix.dart'; import 'package:fluffychat/config/app_config.dart'; @@ -77,99 +78,52 @@ class HtmlMessage extends StatelessWidget { final fontSize = AppConfig.messageFontSize * AppConfig.fontSizeFactor; - final linkColor = textColor.withAlpha(150); - - final blockquoteStyle = Style( - border: Border( - left: BorderSide( - width: 3, - color: textColor, - ), - ), - padding: HtmlPaddings.only(left: 6, bottom: 0), - ); - - final element = _linkifyHtml(HtmlParser.parseHTML(renderHtml)); - // there is no need to pre-validate the html, as we validate it while rendering - return Html.fromElement( - documentElement: element as dom.Element, - style: { - '*': Style( - color: textColor, - margin: Margins.all(0), - fontSize: FontSize(fontSize), - ), - 'a': Style(color: linkColor, textDecorationColor: linkColor), - 'h1': Style( - fontSize: FontSize(fontSize * 2), - lineHeight: LineHeight.number(1.5), - fontWeight: FontWeight.w600, - ), - 'h2': Style( - fontSize: FontSize(fontSize * 1.75), - lineHeight: LineHeight.number(1.5), - fontWeight: FontWeight.w500, - ), - 'h3': Style( - fontSize: FontSize(fontSize * 1.5), - lineHeight: LineHeight.number(1.5), - ), - 'h4': Style( - fontSize: FontSize(fontSize * 1.25), - lineHeight: LineHeight.number(1.5), - ), - 'h5': Style( - fontSize: FontSize(fontSize * 1.25), - lineHeight: LineHeight.number(1.5), - ), - 'h6': Style( - fontSize: FontSize(fontSize), - lineHeight: LineHeight.number(1.5), - ), - 'blockquote': blockquoteStyle, - 'tg-forward': blockquoteStyle, - 'hr': Style( - border: Border.all(color: textColor, width: 0.5), - ), - 'table': Style( - border: Border.all(color: textColor, width: 0.5), - ), - 'tr': Style( - border: Border.all(color: textColor, width: 0.5), - ), - 'td': Style( - border: Border.all(color: textColor, width: 0.5), - padding: HtmlPaddings.all(2), - ), - 'th': Style( - border: Border.all(color: textColor, width: 0.5), - ), + return HtmlWidget( + renderHtml, + customWidgetBuilder: (element) { + if (!allowedHtmlTags.contains(element.localName)) { + Logs().v('Do not render prohibited tag', element.localName); + return Text(element.text); + } + if (element.localName == 'img') { + final source = Uri.tryParse(element.attributes['src'] ?? ''); + if (source?.scheme != 'mxc') { + Logs().v('Do not render img tag with illegal scheme', source); + return Text(element.attributes['alt'] ?? element.text); + } + } + return null; }, - extensions: [ - RoomPillExtension(context, room, fontSize, linkColor), - CodeExtension(fontSize: fontSize), - MatrixMathExtension( - style: TextStyle(fontSize: fontSize, color: textColor), - ), - const TableHtmlExtension(), - SpoilerExtension(textColor: textColor), - const ImageExtension(), - FontColorExtension(), - FallbackTextExtension(fontSize: fontSize), - ], - onLinkTap: (url, _, element) => UrlLauncher( - context, - url, - element?.text, - ).launchUrl(), - onlyRenderTheseTags: const { - ...allowedHtmlTags, - // Needed to make it work properly - 'body', - 'html', + customStylesBuilder: (element) { + switch (element.localName) { + case 'blockquote': + return { + 'border-left': + '4px solid rgb(${textColor.red},${textColor.green},${textColor.blue})', + 'padding-left': '4px', + 'margin': '0px', + 'padding-top': '0px', + }; + default: + return null; + } + }, + textStyle: TextStyle(fontSize: fontSize, color: textColor), + onTapUrl: (url) async { + final consent = await showOkCancelAlertDialog( + fullyCapitalizedForMaterial: false, + context: context, + title: L10n.of(context)!.openLinkInBrowser, + message: url, + okLabel: L10n.of(context)!.openLinkInBrowser, + cancelLabel: L10n.of(context)!.cancel, + ); + if (consent != OkCancelResult.ok) return true; + + UrlLauncher(context, url).launchUrl(); + return true; }, - shrinkWrap: true, ); } @@ -224,282 +178,6 @@ class HtmlMessage extends StatelessWidget { }; } -class FontColorExtension extends HtmlExtension { - static const String colorAttribute = 'color'; - static const String mxColorAttribute = 'data-mx-color'; - static const String bgColorAttribute = 'data-mx-bg-color'; - - @override - Set get supportedTags => {'font', 'span'}; - - @override - bool matches(ExtensionContext context) { - if (!supportedTags.contains(context.elementName)) return false; - return context.element?.attributes.keys.any( - { - colorAttribute, - mxColorAttribute, - bgColorAttribute, - }.contains, - ) ?? - false; - } - - Color? hexToColor(String? hexCode) { - if (hexCode == null) return null; - if (hexCode.startsWith('#')) hexCode = hexCode.substring(1); - if (hexCode.length == 6) hexCode = 'FF$hexCode'; - final colorValue = int.tryParse(hexCode, radix: 16); - return colorValue == null ? null : Color(colorValue); - } - - @override - InlineSpan build( - ExtensionContext context, - ) { - final colorText = context.element?.attributes[colorAttribute] ?? - context.element?.attributes[mxColorAttribute]; - final bgColor = context.element?.attributes[bgColorAttribute]; - return TextSpan( - style: TextStyle( - color: hexToColor(colorText), - backgroundColor: hexToColor(bgColor), - ), - text: context.innerHtml, - ); - } -} - -class ImageExtension extends HtmlExtension { - final double defaultDimension; - - const ImageExtension({this.defaultDimension = 64}); - - @override - Set get supportedTags => {'img'}; - - @override - InlineSpan build(ExtensionContext context) { - final mxcUrl = Uri.tryParse(context.attributes['src'] ?? ''); - if (mxcUrl == null || mxcUrl.scheme != 'mxc') { - return TextSpan(text: context.attributes['alt']); - } - - final width = double.tryParse(context.attributes['width'] ?? ''); - final height = double.tryParse(context.attributes['height'] ?? ''); - - return WidgetSpan( - child: SizedBox( - width: width ?? height ?? defaultDimension, - height: height ?? width ?? defaultDimension, - child: MxcImage( - uri: mxcUrl, - width: width ?? height ?? defaultDimension, - height: height ?? width ?? defaultDimension, - cacheKey: mxcUrl.toString(), - ), - ), - ); - } -} - -class SpoilerExtension extends HtmlExtension { - final Color textColor; - - const SpoilerExtension({required this.textColor}); - - @override - Set get supportedTags => {'span'}; - - static const String customDataAttribute = 'data-mx-spoiler'; - - @override - bool matches(ExtensionContext context) { - if (context.elementName != 'span') return false; - return context.element?.attributes.containsKey(customDataAttribute) ?? - false; - } - - @override - InlineSpan build(ExtensionContext context) { - var obscure = true; - final children = context.inlineSpanChildren; - return WidgetSpan( - child: StatefulBuilder( - builder: (context, setState) { - return InkWell( - onTap: () => setState(() { - obscure = !obscure; - }), - child: RichText( - text: TextSpan( - style: obscure ? TextStyle(backgroundColor: textColor) : null, - children: children, - ), - ), - ); - }, - ), - ); - } -} - -class MatrixMathExtension extends HtmlExtension { - final TextStyle? style; - - MatrixMathExtension({this.style}); - @override - Set get supportedTags => {'div'}; - - @override - bool matches(ExtensionContext context) { - if (context.elementName != 'div') return false; - final mathData = context.element?.attributes['data-mx-maths']; - return mathData != null; - } - - @override - InlineSpan build(ExtensionContext context) { - final data = context.element?.attributes['data-mx-maths'] ?? ''; - return WidgetSpan( - child: Math.tex( - data, - textStyle: style, - onErrorFallback: (e) { - Logs().d('Flutter math parse error', e); - return Text( - data, - style: style, - ); - }, - ), - ); - } -} - -class CodeExtension extends HtmlExtension { - final double fontSize; - - CodeExtension({required this.fontSize}); - @override - Set get supportedTags => {'code'}; - - @override - InlineSpan build(ExtensionContext context) => WidgetSpan( - child: Material( - clipBehavior: Clip.hardEdge, - borderRadius: BorderRadius.circular(4), - child: SingleChildScrollView( - scrollDirection: Axis.horizontal, - child: HighlightView( - context.element?.text ?? '', - language: context.element?.className - .split(' ') - .singleWhereOrNull( - (className) => className.startsWith('language-'), - ) - ?.split('language-') - .last ?? - 'md', - theme: shadesOfPurpleTheme, - padding: EdgeInsets.symmetric( - horizontal: 6, - vertical: context.element?.parent?.localName == 'pre' ? 6 : 0, - ), - textStyle: TextStyle(fontSize: fontSize), - ), - ), - ), - ); -} - -class FallbackTextExtension extends HtmlExtension { - final double fontSize; - - FallbackTextExtension({required this.fontSize}); - @override - Set get supportedTags => HtmlMessage.fallbackTextTags; - - @override - InlineSpan build(ExtensionContext context) => TextSpan( - text: context.element?.text ?? '', - style: TextStyle( - fontSize: fontSize, - ), - ); -} - -class RoomPillExtension extends HtmlExtension { - final Room room; - final BuildContext context; - final double fontSize; - final Color color; - - RoomPillExtension(this.context, this.room, this.fontSize, this.color); - @override - Set get supportedTags => {'a'}; - - @override - bool matches(ExtensionContext context) { - if (context.elementName != 'a') return false; - final userId = context.element?.attributes['href'] - ?.parseIdentifierIntoParts() - ?.primaryIdentifier; - return userId != null; - } - - static final _cachedUsers = {}; - - Future _fetchUser(String matrixId) async => - _cachedUsers[room.id + matrixId] ??= await room.requestUser(matrixId); - - @override - InlineSpan build(ExtensionContext context) { - final href = context.element?.attributes['href']; - final matrixId = href?.parseIdentifierIntoParts()?.primaryIdentifier; - if (href == null || matrixId == null) { - return TextSpan(text: context.innerHtml); - } - if (matrixId.sigil == '@') { - return WidgetSpan( - child: FutureBuilder( - future: _fetchUser(matrixId), - builder: (context, snapshot) => MatrixPill( - key: Key('user_pill_$matrixId'), - name: _cachedUsers[room.id + matrixId]?.calcDisplayname() ?? - matrixId.localpart ?? - matrixId, - avatar: _cachedUsers[room.id + matrixId]?.avatarUrl, - uri: href, - outerContext: this.context, - fontSize: fontSize, - color: color, - ), - ), - ); - } - if (matrixId.sigil == '#' || matrixId.sigil == '!') { - final room = matrixId.sigil == '!' - ? this.room.client.getRoomById(matrixId) - : this.room.client.getRoomByAlias(matrixId); - if (room != null) { - return WidgetSpan( - child: MatrixPill( - name: room.getLocalizedDisplayname(), - avatar: room.avatar, - uri: href, - outerContext: this.context, - fontSize: fontSize, - color: color, - ), - ); - } - } - - return TextSpan(text: context.innerHtml); - } -} - class MatrixPill extends StatelessWidget { final String name; final BuildContext outerContext; diff --git a/pubspec.lock b/pubspec.lock index f3c5caf54..604d32364 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -129,6 +129,30 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.1" + cached_network_image: + dependency: transitive + description: + name: cached_network_image + sha256: "28ea9690a8207179c319965c13cd8df184d5ee721ae2ce60f398ced1219cea1f" + url: "https://pub.dev" + source: hosted + version: "3.3.1" + cached_network_image_platform_interface: + dependency: transitive + description: + name: cached_network_image_platform_interface + sha256: "9e90e78ae72caa874a323d78fa6301b3fb8fa7ea76a8f96dc5b5bf79f283bf2f" + url: "https://pub.dev" + source: hosted + version: "4.0.0" + cached_network_image_web: + dependency: transitive + description: + name: cached_network_image_web + sha256: "42a835caa27c220d1294311ac409a43361088625a4f23c820b006dd9bffb3316" + url: "https://pub.dev" + source: hosted + version: "1.1.1" callkeep: dependency: "direct main" description: @@ -475,22 +499,6 @@ packages: url: "https://pub.dev" source: hosted version: "0.1.1" - flutter_html: - dependency: "direct main" - description: - name: flutter_html - sha256: "02ad69e813ecfc0728a455e4bf892b9379983e050722b1dce00192ee2e41d1ee" - url: "https://pub.dev" - source: hosted - version: "3.0.0-beta.2" - flutter_html_table: - dependency: "direct main" - description: - name: flutter_html_table - sha256: e20c72d67ea2512e7b4949f6f7dd13d004e773b0f82c586a21f895e6bd90383c - url: "https://pub.dev" - source: hosted - version: "3.0.0-beta.2" flutter_keyboard_visibility: dependency: transitive description: @@ -539,14 +547,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.0" - flutter_layout_grid: - dependency: transitive - description: - name: flutter_layout_grid - sha256: "3529b7aa7ed2cb9861a0bbaa5c14d4be2beaf5a070ce0176077159f80c5de094" - url: "https://pub.dev" - source: hosted - version: "2.0.5" flutter_linkify: dependency: "direct main" description: @@ -755,6 +755,22 @@ packages: url: "https://pub.dev" source: hosted version: "0.9.46" + flutter_widget_from_html: + dependency: "direct main" + description: + name: flutter_widget_from_html + sha256: "22c911b6ccf82b83e0c457d987bac4e703440fea0fc88dab24f4dfe995a5f33f" + url: "https://pub.dev" + source: hosted + version: "0.14.11" + flutter_widget_from_html_core: + dependency: transitive + description: + name: flutter_widget_from_html_core + sha256: "028f4989b9ff4907466af233d50146d807772600d98a3e895662fbdb09c39225" + url: "https://pub.dev" + source: hosted + version: "0.14.11" fuchsia_remote_debug_protocol: dependency: transitive description: flutter @@ -768,6 +784,54 @@ packages: url: "https://pub.dev" source: hosted version: "0.3.0" + fwfh_cached_network_image: + dependency: transitive + description: + name: fwfh_cached_network_image + sha256: "952aea958a5fda7d616cc297ba4bc08427e381459e75526fa375d6d8345630d3" + url: "https://pub.dev" + source: hosted + version: "0.14.2" + fwfh_chewie: + dependency: transitive + description: + name: fwfh_chewie + sha256: bbb036cd322ab77dc0edd34cbbf76181681f5e414987ece38745dc4f3d7408ed + url: "https://pub.dev" + source: hosted + version: "0.14.7" + fwfh_just_audio: + dependency: transitive + description: + name: fwfh_just_audio + sha256: "4962bc59cf8bbb0a77a55ff56a7b925612b0d8263bc2ede3636b9c86113cb493" + url: "https://pub.dev" + source: hosted + version: "0.14.2" + fwfh_svg: + dependency: transitive + description: + name: fwfh_svg + sha256: "3fd83926b7245d287f133a437ef430befd99d3b00ba8c600f26cc324af281f72" + url: "https://pub.dev" + source: hosted + version: "0.8.1" + fwfh_url_launcher: + dependency: transitive + description: + name: fwfh_url_launcher + sha256: "2a526c9819f74b4106ba2fba4dac79f0082deecd8d2c7011cd0471cb710e3eff" + url: "https://pub.dev" + source: hosted + version: "0.9.0+4" + fwfh_webview: + dependency: transitive + description: + name: fwfh_webview + sha256: b828bb5ddd4361a866cdb8f1b0de4f3348f332915ecf2f4215ba17e46c656adc + url: "https://pub.dev" + source: hosted + version: "0.14.8" geolocator: dependency: "direct main" description: @@ -1110,14 +1174,6 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.0" - list_counter: - dependency: transitive - description: - name: list_counter - sha256: c447ae3dfcd1c55f0152867090e67e219d42fe6d4f2807db4bbe8b8d69912237 - url: "https://pub.dev" - source: hosted - version: "1.0.2" lists: dependency: transitive description: @@ -1238,6 +1294,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.0" + octo_image: + dependency: transitive + description: + name: octo_image + sha256: "45b40f99622f11901238e18d48f5f12ea36426d8eced9f4cbf58479c7aa2430d" + url: "https://pub.dev" + source: hosted + version: "2.0.0" olm: dependency: transitive description: @@ -1550,14 +1614,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.1" - quiver: - dependency: transitive - description: - name: quiver - sha256: b1c1ac5ce6688d77f65f3375a9abb9319b3cb32486bdc7a1e0fdf004d7ba4e47 - url: "https://pub.dev" - source: hosted - version: "3.2.1" random_string: dependency: transitive description: @@ -2260,6 +2316,38 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.2" + webview_flutter: + dependency: transitive + description: + name: webview_flutter + sha256: "25e1b6e839e8cbfbd708abc6f85ed09d1727e24e08e08c6b8590d7c65c9a8932" + url: "https://pub.dev" + source: hosted + version: "4.7.0" + webview_flutter_android: + dependency: transitive + description: + name: webview_flutter_android + sha256: "3e5f4e9d818086b0d01a66fb1ff9cc72ab0cc58c71980e3d3661c5685ea0efb0" + url: "https://pub.dev" + source: hosted + version: "3.15.0" + webview_flutter_platform_interface: + dependency: transitive + description: + name: webview_flutter_platform_interface + sha256: d937581d6e558908d7ae3dc1989c4f87b786891ab47bb9df7de548a151779d8d + url: "https://pub.dev" + source: hosted + version: "2.10.0" + webview_flutter_wkwebview: + dependency: transitive + description: + name: webview_flutter_wkwebview + sha256: "9bf168bccdf179ce90450b5f37e36fe263f591c9338828d6bf09b6f8d0f57f86" + url: "https://pub.dev" + source: hosted + version: "3.12.0" win32: dependency: transitive description: @@ -2317,5 +2405,5 @@ packages: source: hosted version: "3.1.2" sdks: - dart: ">=3.2.0 <4.0.0" - flutter: ">=3.16.0" + dart: ">=3.2.3 <4.0.0" + flutter: ">=3.16.6" diff --git a/pubspec.yaml b/pubspec.yaml index 57cbc2c4a..b09214463 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -34,8 +34,6 @@ dependencies: flutter_file_dialog: ^3.0.2 flutter_foreground_task: ^6.0.0+1 flutter_highlighter: ^0.1.1 - flutter_html: ^3.0.0-beta.2 - flutter_html_table: ^3.0.0-beta.2 flutter_linkify: ^6.0.0 flutter_local_notifications: ^16.3.2 flutter_localizations: @@ -51,6 +49,7 @@ dependencies: flutter_typeahead: ^5.2.0 flutter_web_auth_2: ^3.0.4 flutter_webrtc: ^0.9.46 + flutter_widget_from_html: ^0.14.11 future_loading_dialog: ^0.3.0 geolocator: ^7.6.2 go_router: ^13.1.0