From e7890cfe4f858e1d950edb3dd83be1151298a718 Mon Sep 17 00:00:00 2001 From: Henry-Hiles Date: Thu, 4 Dec 2025 14:56:33 -0500 Subject: [PATCH] add sticker and emote support --- README.md | 2 +- lib/controllers/thumbnail_controller.dart | 24 ++++++++ lib/helpers/extensions/event_to_message.dart | 4 +- lib/models/image_data.dart | 11 ++++ lib/widgets/chat_page/html/html.dart | 61 +++++++++++++++++++- lib/widgets/chat_page/room_chat.dart | 1 + 6 files changed, 97 insertions(+), 6 deletions(-) create mode 100644 lib/controllers/thumbnail_controller.dart create mode 100644 lib/models/image_data.dart diff --git a/README.md b/README.md index 2a708d9..f7e068c 100644 --- a/README.md +++ b/README.md @@ -65,7 +65,7 @@ A simple and user-friendly Matrix client made with Flutter and the Matrix Dart S - [ ] Plain text - [ ] Matrix URIs - [ ] Matrix.to links - - [ ] Custom emojis/stickers + - [x] Custom emojis/stickers - [ ] Encrypted messages - [x] History loading - [x] Backwards diff --git a/lib/controllers/thumbnail_controller.dart b/lib/controllers/thumbnail_controller.dart new file mode 100644 index 0000000..e1a6468 --- /dev/null +++ b/lib/controllers/thumbnail_controller.dart @@ -0,0 +1,24 @@ +import "package:flutter_riverpod/flutter_riverpod.dart"; +import "package:matrix/matrix.dart"; +import "package:nexus/controllers/client_controller.dart"; +import "package:nexus/models/image_data.dart"; + +class ThumbnailController extends AsyncNotifier { + ThumbnailController(this.data); + final ImageData data; + + @override + Future build({String? from}) async { + final client = await ref.watch(ClientController.provider.future); + final uri = await Uri.tryParse(data.uri)?.getDownloadUri( + client, + ); // TODO: Should use thumb when c10y fixes animated thumbs + + return uri.toString(); + } + + static final provider = AsyncNotifierProvider.family + .autoDispose( + ThumbnailController.new, + ); +} diff --git a/lib/helpers/extensions/event_to_message.dart b/lib/helpers/extensions/event_to_message.dart index cd4ab0f..41fbb8e 100644 --- a/lib/helpers/extensions/event_to_message.dart +++ b/lib/helpers/extensions/event_to_message.dart @@ -50,8 +50,8 @@ extension EventToMessage on Event { text: "Unable to decrypt message.", metadata: {"formatted": "Unable to decrypt message.", ...metadata}, ), - EventTypes.Message => switch (messageType) { - MessageTypes.Image => Message.image( + (EventTypes.Sticker || EventTypes.Message) => switch (messageType) { + (MessageTypes.Sticker || MessageTypes.Image) => Message.image( metadata: metadata, id: eventId, authorId: senderId, diff --git a/lib/models/image_data.dart b/lib/models/image_data.dart new file mode 100644 index 0000000..e5bc57e --- /dev/null +++ b/lib/models/image_data.dart @@ -0,0 +1,11 @@ +import "package:freezed_annotation/freezed_annotation.dart"; +part "image_data.freezed.dart"; + +@freezed +abstract class ImageData with _$ImageData { + const factory ImageData({ + required String uri, + required int? height, + required int? width, + }) = _ImageData; +} diff --git a/lib/widgets/chat_page/html/html.dart b/lib/widgets/chat_page/html/html.dart index 37042d1..01a0345 100644 --- a/lib/widgets/chat_page/html/html.dart +++ b/lib/widgets/chat_page/html/html.dart @@ -2,14 +2,20 @@ import "package:fast_immutable_collections/fast_immutable_collections.dart"; import "package:flutter/material.dart"; import "package:flutter_riverpod/flutter_riverpod.dart"; import "package:flutter_widget_from_html_core/flutter_widget_from_html_core.dart"; +import "package:matrix/matrix.dart"; +import "package:nexus/controllers/thumbnail_controller.dart"; +import "package:nexus/helpers/extensions/get_headers.dart"; import "package:nexus/helpers/launch_helper.dart"; +import "package:nexus/models/image_data.dart"; import "package:nexus/widgets/chat_page/html/spoiler_text.dart"; import "package:nexus/widgets/chat_page/html/code_block.dart"; import "package:nexus/widgets/chat_page/quoted.dart"; +import "package:nexus/widgets/error_dialog.dart"; class Html extends ConsumerWidget { final String html; - const Html(this.html, {super.key}); + final Client client; + const Html(this.html, {required this.client, super.key}); @override Widget build(BuildContext context, WidgetRef ref) => HtmlWidget( @@ -18,13 +24,63 @@ class Html extends ConsumerWidget { if (element.attributes.keys.contains("data-mx-spoiler")) { return SpoilerText(text: element.text); } + + final height = int.tryParse(element.attributes["height"] ?? "") ?? 300; + final width = int.tryParse(element.attributes["width"] ?? ""); + return switch (element.localName) { "code" => CodeBlock( element.text, lang: element.className.replaceAll("language-", ""), ), - "blockquote" => Quoted(Html(element.innerHtml)), + "blockquote" => Quoted(Html(element.innerHtml, client: client)), + + "img" => + element.attributes["src"] == null + ? null + : Consumer( + builder: (_, ref, _) => ref + .watch( + ThumbnailController.provider( + ImageData( + uri: element.attributes["src"]!, + height: height, + width: width, + ), + ), + ) + .when( + data: (uri) { + if (uri == null) return SizedBox.shrink(); + + return InlineCustomWidget( + child: Image.network( + uri, + headers: client.headers, + errorBuilder: (_, error, _) => Text( + "Image Failed to Load", + style: TextStyle(color: Colors.red), + ), + height: height.toDouble(), + width: width?.toDouble(), + loadingBuilder: (_, child, loadingProgress) => + loadingProgress == null + ? child + : CircularProgressIndicator(), + ), + ); + }, + error: ErrorDialog.new, + loading: () => InlineCustomWidget( + child: SizedBox( + width: width?.toDouble(), + height: height.toDouble(), + child: CircularProgressIndicator(), + ), + ), + ), + ), ("del" || "h1" || @@ -59,7 +115,6 @@ class Html extends ConsumerWidget { "caption" || "pre" || "span" || - "img" || "details" || "summary") => null, diff --git a/lib/widgets/chat_page/room_chat.dart b/lib/widgets/chat_page/room_chat.dart index b5d9de6..3a544ab 100644 --- a/lib/widgets/chat_page/room_chat.dart +++ b/lib/widgets/chat_page/room_chat.dart @@ -200,6 +200,7 @@ class RoomChat extends HookConsumerWidget { ((message.editedAt != null) ? "(edited)" : ""), + client: room.roomData.client, ), topWidget: TopWidget( message,