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