From f2efbbeb9fd4c9529ffbd538536da9b17544a70c Mon Sep 17 00:00:00 2001 From: Henry-Hiles Date: Tue, 22 Sep 2026 20:48:59 -0400 Subject: [PATCH] fixup select --- lib/widgets/html/html.dart | 242 ++++++++++++------------- lib/widgets/renderers/message.dart | 277 ++++++++++++----------------- 2 files changed, 229 insertions(+), 290 deletions(-) diff --git a/lib/widgets/html/html.dart b/lib/widgets/html/html.dart index 6cb60b2..85f8054 100644 --- a/lib/widgets/html/html.dart +++ b/lib/widgets/html/html.dart @@ -1,5 +1,4 @@ import "package:fast_immutable_collections/fast_immutable_collections.dart"; -import "package:flutter/rendering.dart"; import "package:material_ui/material_ui.dart"; import "package:flutter_riverpod/flutter_riverpod.dart"; import "package:flutter_widget_from_html_core/flutter_widget_from_html_core.dart"; @@ -19,140 +18,133 @@ class const Html( super.key, }) extends ConsumerWidget { @override - Widget build(BuildContext context, WidgetRef ref) { - // needed until https://github.com/daohoangson/flutter_widget_from_html/issues/1618 is resolved - final htmlWidget = MaterialUiCompatibilityBridge( - child: HtmlWidget( - html, - buildAsync: false, - textStyle: textStyle, - customWidgetBuilder: (element) { - if (element.attributes.keys.contains("data-mx-profile-fallback")) { - return SizedBox.shrink(); - } + Widget + build(BuildContext context, WidgetRef ref) + // needed until https://github.com/daohoangson/flutter_widget_from_html/issues/1618 is resolved + => MaterialUiCompatibilityBridge( + child: HtmlWidget( + html, + buildAsync: false, + textStyle: textStyle, + customWidgetBuilder: (element) { + if (element.attributes.keys.contains("data-mx-profile-fallback")) { + return SizedBox.shrink(); + } - if (element.attributes.keys.contains("data-mx-spoiler")) { - return InlineCustomWidget(child: SpoilerText(element.text)); - } + if (element.attributes.keys.contains("data-mx-spoiler")) { + return InlineCustomWidget(child: SpoilerText(element.text)); + } - final height = - int.tryParse(element.attributes["height"] ?? "") ?? - (element.attributes.keys.contains("data-mx-emoticon") - ? 32 - : null) ?? - 300; - final width = int.tryParse(element.attributes["width"] ?? ""); - final src = Uri.tryParse(element.attributes["src"] ?? ""); + final height = + int.tryParse(element.attributes["height"] ?? "") ?? + (element.attributes.keys.contains("data-mx-emoticon") + ? 32 + : null) ?? + 300; + final width = int.tryParse(element.attributes["width"] ?? ""); + final src = Uri.tryParse(element.attributes["src"] ?? ""); - return switch (element.localName) { - "code" => - element.parent?.localName == "pre" - ? CodeBlock( - element.text, - lang: element.className.replaceAll("language-", ""), - ) - : null, + return switch (element.localName) { + "code" => + element.parent?.localName == "pre" + ? CodeBlock( + element.text, + lang: element.className.replaceAll("language-", ""), + ) + : null, - "blockquote" => Quoted( - Html(element.innerHtml, textStyle: textStyle, roomId: roomId), - ), + "blockquote" => Quoted( + Html(element.innerHtml, textStyle: textStyle, roomId: roomId), + ), - "a" => - element.attributes["href"]?.mention == null - ? null - : InlineCustomWidget( - child: MentionChip(element.attributes["href"]!, roomId), - ), + "a" => + element.attributes["href"]?.mention == null + ? null + : InlineCustomWidget( + child: MentionChip(element.attributes["href"]!, roomId), + ), - "img" => - src == null - ? SizedBox.shrink() - : InlineCustomWidget( - alignment: PlaceholderAlignment.middle, - child: ExpandableImage( - .new(mxc: src), - child: Image( - image: MxcImage(ref, .new(mxc: src)), - errorBuilder: (_, error, _) => Text( - "Image Failed to Load", - style: .new( - color: Theme.of(context).colorScheme.error, - ), + "img" => + src == null + ? SizedBox.shrink() + : InlineCustomWidget( + alignment: PlaceholderAlignment.middle, + child: ExpandableImage( + .new(mxc: src), + child: Image( + image: MxcImage(ref, .new(mxc: src)), + errorBuilder: (_, error, _) => Text( + "Image Failed to Load", + style: .new( + color: Theme.of(context).colorScheme.error, ), - height: height.toDouble(), - width: width?.toDouble(), - loadingBuilder: (_, child, loadingProgress) => - loadingProgress == null - ? child - : CircularProgressIndicator(), ), + height: height.toDouble(), + width: width?.toDouble(), + loadingBuilder: (_, child, loadingProgress) => + loadingProgress == null + ? child + : CircularProgressIndicator(), ), ), + ), - // Allowed elements list - ("del" || - "h1" || - "h2" || - "h3" || - "h4" || - "h5" || - "h6" || - "p" || - "ul" || - "ol" || - "sup" || - "sub" || - "li" || - "b" || - "i" || - "u" || - "strong" || - "em" || - "s" || - "code" || - "hr" || - "br" || - "div" || - "table" || - "thead" || - "tbody" || - "tr" || - "th" || - "td" || - "caption" || - "pre" || - "span" || - "details" || - "summary") => - null, + // Allowed elements list + ("del" || + "h1" || + "h2" || + "h3" || + "h4" || + "h5" || + "h6" || + "p" || + "ul" || + "ol" || + "sup" || + "sub" || + "li" || + "b" || + "i" || + "u" || + "strong" || + "em" || + "s" || + "code" || + "hr" || + "br" || + "div" || + "table" || + "thead" || + "tbody" || + "tr" || + "th" || + "td" || + "caption" || + "pre" || + "span" || + "details" || + "summary") => + null, - _ => SizedBox.shrink(), - }; - }, - customStylesBuilder: (element) => { - "width": "auto", - ...Map.fromEntries( - element.attributes - .mapTo?>( - (key, value) => switch (key) { - "data-mx-color" => .new("color", value), - "data-mx-bg-color" => .new("background-color", value), - _ => null, - }, - ) - .nonNulls, - ), - }, - onTapUrl: (url) => - ref.watch(LaunchHelper.provider).launchUrl(.parse(url)), - ), - ); - - return RendererBinding.instance.mouseTracker.mouseIsConnected - ? SelectableRegion( - selectionControls: materialTextSelectionControls, - child: htmlWidget, - ) - : htmlWidget; - } + _ => SizedBox.shrink(), + }; + }, + customStylesBuilder: (element) => { + "width": "auto", + ...Map.fromEntries( + element.attributes + .mapTo?>( + (key, value) => switch (key) { + "data-mx-color" => .new("color", value), + "data-mx-bg-color" => .new("background-color", value), + _ => null, + }, + ) + .nonNulls, + ), + }, + onTapUrl: (url) => + ref.watch(LaunchHelper.provider).launchUrl(.parse(url)), + ), + ); } diff --git a/lib/widgets/renderers/message.dart b/lib/widgets/renderers/message.dart index 99c3141..a0bbf99 100644 --- a/lib/widgets/renderers/message.dart +++ b/lib/widgets/renderers/message.dart @@ -1,4 +1,5 @@ import "package:collection/collection.dart"; +import "package:flutter/rendering.dart"; import "package:material_ui/material_ui.dart"; import "package:flutter_riverpod/flutter_riverpod.dart"; import "package:linkify/linkify.dart"; @@ -40,6 +41,111 @@ class const MessageRenderer( fontStyle: event.content is EmoteMessageContent ? .italic : null, ); + final rendered = switch (event.content) { + EncryptedContent() => Text("Unable to decrypt event", style: errorStyle), + StickerContent(:final body, :final url, :final info) => + textOnly + ? Text(body, maxLines: maxLines, overflow: .ellipsis) + : ConstrainedBox( + constraints: .loose(.square(200)), + child: MessageImage(url, info: info, encrypted: false), + ), + // TODO: Handle locations + // LocationMessageContent(:final body , :final geoUri) => + TextMessageContent(:final body, :final formattedBody, :final format) || + NoticeMessageContent(:final body, :final formattedBody, :final format) || + EmoteMessageContent(:final body, :final formattedBody, :final format) || + ImageMessageContent(:final body, :final formattedBody, :final format) || + VideoMessageContent(:final body, :final formattedBody, :final format) || + AudioMessageContent(:final body, :final formattedBody, :final format) || + FileMessageContent( + :final body, + :final formattedBody, + :final format, + ) => Column( + crossAxisAlignment: .start, + children: [ + format == .html && !textOnly + ? Html( + roomId: event.roomId, + textStyle: textStyle, + formattedBody!.replaceAllMapped( + RegExp( + r"(]*>.*?<\/a>)|(\bhttps?:\/\/[^\s<]+)", + caseSensitive: false, + dotAll: true, + ), + (m) { + // If it's already an tag, leave it unchanged + if (m.group(1) != null) { + return m.group(1)!; + } + + // Otherwise, wrap the bare URL + final url = m.group(2)!; + return "$url"; + }, + ), + ) + : LinkifiedText(body, style: textStyle, maxLines: maxLines), + + if (!textOnly) ...[ + if (event.content + case ImageMessageContent(:final url) || + FileMessageContent(:final url) || + VideoMessageContent(:final url) || + AudioMessageContent(:final url)) + ConstrainedBox( + constraints: .loose(.square(500)), + child: switch (event.content) { + VideoMessageContent(:final info, :final file) => VideoPlayer( + url, + info, + encrypted: file != null, + ), + AudioMessageContent(:final info, :final file) => AudioPlayer( + url, + info, + + encrypted: file != null, + ), + FileMessageContent(:final info, :final filename) => FileCard( + url, + info, + filename: filename, + ), + ImageMessageContent(:final info, :final file) => MessageImage( + url, + info: info, + encrypted: file != null, + ), + _ => SizedBox.shrink(), + }, + ), + + if (event.lastEditRowId != 0) + Text("(edited)", style: theme.textTheme.labelSmall), + + if (linkify(body) + .firstWhereOrNull((element) => element is UrlElement) + case final UrlElement link?) + if (Uri.tryParse(link.url) case final Uri url?) UrlPreview(url), + ], + ], + ), + MessageContent(:final body) => + body == null + ? Text("This message is redacted", style: errorStyle) + : Wrap( + spacing: 8, + children: [ + Text("Unknown message type:", style: errorStyle), + Text(body), + ], + ), + _ => throw Exception("This is impossible"), + }; + return Row( crossAxisAlignment: .start, mainAxisSize: .min, @@ -113,172 +219,13 @@ class const MessageRenderer( ), ), ), - switch (event.content) { - EncryptedContent() => Text( - "Unable to decrypt event", - style: errorStyle, - ), - StickerContent(:final body, :final url, :final info) => - textOnly - ? Text( - body, - maxLines: maxLines, - overflow: .ellipsis, - ) - : ConstrainedBox( - constraints: .loose(.square(200)), - child: MessageImage( - url, - info: info, - encrypted: false, - ), - ), - // TODO: Handle locations - // LocationMessageContent(:final body , :final geoUri) => - TextMessageContent( - :final body, - :final formattedBody, - :final format, - ) || - NoticeMessageContent( - :final body, - :final formattedBody, - :final format, - ) || - EmoteMessageContent( - :final body, - :final formattedBody, - :final format, - ) || - ImageMessageContent( - :final body, - :final formattedBody, - :final format, - ) || - VideoMessageContent( - :final body, - :final formattedBody, - :final format, - ) || - AudioMessageContent( - :final body, - :final formattedBody, - :final format, - ) || - FileMessageContent( - :final body, - :final formattedBody, - :final format, - ) => Column( - crossAxisAlignment: .start, - children: [ - format == .html && !textOnly - ? Html( - roomId: event.roomId, - textStyle: textStyle, - formattedBody!.replaceAllMapped( - RegExp( - r"(]*>.*?<\/a>)|(\bhttps?:\/\/[^\s<]+)", - caseSensitive: false, - dotAll: true, - ), - (m) { - // If it's already an tag, leave it unchanged - if (m.group(1) != null) { - return m.group(1)!; - } - // Otherwise, wrap the bare URL - final url = m.group(2)!; - return "$url"; - }, - ), - ) - : LinkifiedText( - body, - style: textStyle, - maxLines: maxLines, - ), - - if (!textOnly) ...[ - if (event.content - case ImageMessageContent(:final url) || - FileMessageContent(:final url) || - VideoMessageContent(:final url) || - AudioMessageContent(:final url)) - ConstrainedBox( - constraints: .loose(.square(500)), - child: switch (event.content) { - VideoMessageContent( - :final info, - :final file, - ) => - VideoPlayer( - url, - info, - encrypted: file != null, - ), - AudioMessageContent( - :final info, - :final file, - ) => - AudioPlayer( - url, - info, - - encrypted: file != null, - ), - FileMessageContent( - :final info, - :final filename, - ) => - FileCard(url, info, filename: filename), - ImageMessageContent( - :final info, - :final file, - ) => - MessageImage( - url, - info: info, - encrypted: file != null, - ), - _ => SizedBox.shrink(), - }, - ), - - if (event.lastEditRowId != 0) - Text( - "(edited)", - style: theme.textTheme.labelSmall, - ), - - if (linkify(body).firstWhereOrNull( - (element) => element is UrlElement, - ) - case final UrlElement link?) - if (Uri.tryParse(link.url) case final Uri url?) - UrlPreview(url), - ], - ], - ), - MessageContent(:final body) => - body == null - ? Text( - "This message is redacted", - style: errorStyle, - ) - : Wrap( - spacing: 8, - children: [ - Text( - "Unknown message type:", - style: errorStyle, - ), - Text(body), - ], - ), - _ => throw Exception("This is impossible"), - }, + RendererBinding.instance.mouseTracker.mouseIsConnected + ? SelectableRegion( + selectionControls: materialTextSelectionControls, + child: rendered, + ) + : rendered, if (!textOnly) ReactionRow(event), ], ),