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),
],
),