From a8538d7488b965ff0108ae4ad04a6444ce65899a Mon Sep 17 00:00:00 2001 From: Krille Date: Mon, 22 May 2023 16:26:43 +0200 Subject: [PATCH] chore: Follow up html messages mxc images --- lib/pages/chat/events/html_message.dart | 136 +++++++++++++++++++++++- 1 file changed, 134 insertions(+), 2 deletions(-) diff --git a/lib/pages/chat/events/html_message.dart b/lib/pages/chat/events/html_message.dart index 58815163..523f030f 100644 --- a/lib/pages/chat/events/html_message.dart +++ b/lib/pages/chat/events/html_message.dart @@ -10,6 +10,7 @@ import 'package:matrix/matrix.dart'; import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/widgets/avatar.dart'; +import 'package:fluffychat/widgets/mxc_image.dart'; import '../../../utils/url_launcher.dart'; class HtmlMessage extends StatelessWidget { @@ -105,6 +106,12 @@ class HtmlMessage extends StatelessWidget { 'th': Style( border: Border.all(color: textColor, width: 0.5), ), + 'li': Style( + // https://github.com/Sub6Resources/flutter_html/issues/1280 + // Workaround for list items printed in the same line. This will + // remove the dots/numbers. Hours wasted: 4 + display: Display.block, + ), }, extensions: [ UserPillExtension(context, room), @@ -114,6 +121,9 @@ class HtmlMessage extends StatelessWidget { style: TextStyle(fontSize: fontSize, color: textColor), ), const TableHtmlExtension(), + SpoilerExtension(textColor: textColor), + const ImageExtension(), + FontColorExtension(), ], onLinkTap: (url, _, __) => UrlLauncher(context, url).launchUrl(), onlyRenderTheseTags: const { @@ -163,13 +173,135 @@ class HtmlMessage extends StatelessWidget { 'caption', 'pre', 'span', - // TODO: Implement image extension for Mxc URIs - //'img', + 'img', 'details', 'summary' }; } +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, + Map Function() parseChildren, + ) { + 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, + Map Function() parseChildren, + ) { + final mxcUrl = Uri.tryParse(context.attributes['href'] ?? ''); + if (mxcUrl == null || mxcUrl.scheme != 'mxc') { + return TextSpan(text: context.attributes['alt']); + } + + final width = + double.tryParse(context.attributes['width'] ?? '') ?? defaultDimension; + final height = + double.tryParse(context.attributes['height'] ?? '') ?? defaultDimension; + + return WidgetSpan( + child: MxcImage( + uri: mxcUrl, + width: width, + height: height, + ), + ); + } +} + +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, + Map Function() parseChildren, + ) { + var obscure = true; + return WidgetSpan( + child: StatefulBuilder( + builder: (context, setState) { + return InkWell( + onTap: () => setState(() { + obscure = !obscure; + }), + child: RichText( + text: TextSpan( + style: obscure ? TextStyle(backgroundColor: textColor) : null, + children: parseChildren().values.toList(), + ), + ), + ); + }, + ), + ); + } +} + class MatrixMathExtension extends HtmlExtension { final TextStyle? style;