From 0769ab4dbb032c2e29e9e94dab5898c9a36c4a0f Mon Sep 17 00:00:00 2001 From: Henry-Hiles Date: Tue, 10 Mar 2026 19:22:41 -0400 Subject: [PATCH] improve reply handling --- lib/controllers/message_controller.dart | 80 ++++++++++++++++--------- lib/models/message_config.dart | 2 +- lib/widgets/chat_page/room_chat.dart | 36 ++++++----- lib/widgets/chat_page/top_widget.dart | 14 +++-- 4 files changed, 83 insertions(+), 49 deletions(-) diff --git a/lib/controllers/message_controller.dart b/lib/controllers/message_controller.dart index c42c8d7..1edf5e9 100644 --- a/lib/controllers/message_controller.dart +++ b/lib/controllers/message_controller.dart @@ -47,25 +47,21 @@ class MessageController extends AsyncNotifier { final type = (config.event.decryptedType ?? config.event.type); final newContent = content["m.new_content"] as Map?; final metadata = { - "timelineId": event.timelineRowId, - "formatted": - newContent?["formatted_body"] ?? - newContent?["body"] ?? - content["formatted_body"] ?? - content["body"] ?? - "", + "body": config.event.redactedBy == null + ? (newContent?["body"] ?? content["body"] ?? "") + : "Deleted Message", if (replyEvent != null) "reply": await ref.watch( MessageController.provider( MessageConfig( event: replyEvent, room: config.room, - mustBeText: true, + alwaysReturn: true, ), ).future, ), + "timelineId": event.timelineRowId, "big": event.localContent?.bigEmoji == true, - "body": newContent?["body"] ?? content["body"], "eventType": type, "avatarUrl": author?.content["avatar_url"], "editSource": @@ -82,7 +78,7 @@ class MessageController extends AsyncNotifier { final editedAt = event.relationType == "m.replace" ? event.timestamp : null; - if ((event.redactedBy != null && !config.mustBeText) || + if ((event.redactedBy != null && !config.alwaysReturn) || (!config.includeEdits && (config.event.relationType == "m.replace"))) { return null; } @@ -98,17 +94,18 @@ class MessageController extends AsyncNotifier { metadata: metadata, id: config.event.eventId, authorId: event.authorId, - text: config.event.redactedBy == null - ? content["body"] ?? "" - : "This message has been deleted...", + text: + newContent?["formatted_body"] ?? + newContent?["body"] ?? + content["formatted_body"] ?? + content["body"] ?? + "", replyToMessageId: replyId, deliveredAt: config.event.timestamp, editedAt: editedAt, ) as TextMessage; - if (config.mustBeText) return asText; - final homeserver = ref.read(ClientStateController.provider)?.homeserverUrl; final source = homeserver == null || content["url"] == null ? "null" @@ -117,7 +114,7 @@ class MessageController extends AsyncNotifier { return switch (type) { "m.room.encrypted" => asText.copyWith( text: "Unable to decrypt message.", - metadata: {...metadata, "formatted": "Unable to decrypt message."}, + metadata: {...metadata, "body": "Unable to decrypt message."}, ), // "org.matrix.msc3381.poll.start" => Message.custom( // metadata: { @@ -134,7 +131,11 @@ class MessageController extends AsyncNotifier { id: config.event.eventId, metadata: metadata, authorId: event.authorId, - text: event.localContent?.sanitizedHtml, + text: + newContent?["formatted_body"] ?? + newContent?["body"] ?? + content["formatted_body"] ?? + content["body"], source: source, replyToMessageId: replyId, deliveredAt: config.event.timestamp, @@ -156,7 +157,18 @@ class MessageController extends AsyncNotifier { content["membership"] == event.unsigned["prev_content"]?["membership"] ? null : Message.system( - metadata: metadata, + metadata: { + ...metadata, + "body": + "${content["displayname"] ?? event.stateKey} ${switch (content["membership"]) { + "invite" => "was invited to", + "join" => "joined", + "leave" => "left", + "knock" => "asked to join", + "ban" => "was banned from", + _ => "did something relating to", + }} the room.", + }, id: config.event.eventId, authorId: event.authorId, deliveredAt: config.event.timestamp, @@ -170,18 +182,30 @@ class MessageController extends AsyncNotifier { _ => "did something relating to", }} the room.", ), - "m.room.redaction" => null, - _ => - // Turn this on for debugging purposes - false - // ignore: dead_code - ? Message.unsupported( - metadata: metadata, - id: config.event.eventId, - authorId: event.authorId, - replyToMessageId: replyId, + + "m.room.redaction" => + config.alwaysReturn + ? asText.copyWith( + metadata: { + ...(asText.metadata ?? {}), + "body": "Deleted Message", + }, ) : null, + _ => + config.alwaysReturn + ? asText + : ( + // Turn this on for debugging purposes + false + // ignore: dead_code + ? Message.unsupported( + metadata: metadata, + id: config.event.eventId, + authorId: event.authorId, + replyToMessageId: replyId, + ) + : null), }; } diff --git a/lib/models/message_config.dart b/lib/models/message_config.dart index 4f3abf3..f7490e5 100644 --- a/lib/models/message_config.dart +++ b/lib/models/message_config.dart @@ -7,7 +7,7 @@ part "message_config.g.dart"; @freezed abstract class MessageConfig with _$MessageConfig { const factory MessageConfig({ - @Default(false) bool mustBeText, + @Default(false) bool alwaysReturn, @Default(false) bool includeEdits, required Room room, required Event event, diff --git a/lib/widgets/chat_page/room_chat.dart b/lib/widgets/chat_page/room_chat.dart index 40ee0a1..3f55d60 100644 --- a/lib/widgets/chat_page/room_chat.dart +++ b/lib/widgets/chat_page/room_chat.dart @@ -395,8 +395,7 @@ class RoomChat extends HookConsumerWidget { message.metadata?["big"] == true ? TextStyle(fontSize: 32) : null, - (message.metadata?["formatted"] - as String) + message.text .replaceAllMapped( RegExp( "(]*>.*?<\\/a>)|(\\bhttps?:\\/\\/[^\\s<]+)", @@ -466,20 +465,29 @@ class RoomChat extends HookConsumerWidget { : CrossAxisAlignment.start, children: [ SizedBox(height: 12), - FlyerChatTextMessage( - topWidget: TopWidget( - message, - groupStatus: groupStatus, - alwaysShow: true, + if (message.text?.isNotEmpty == true) + FlyerChatTextMessage( + topWidget: TopWidget( + message, + groupStatus: groupStatus, + alwaysShow: true, + ), + message: TextMessage( + id: "${message.id}-text", + authorId: message.authorId, + text: message.text!, + ), + index: index, ), - message: TextMessage( - id: "${message.id}-text", - authorId: message.authorId, - text: message.metadata?["formatted"], - ), - index: index, - ), FlyerChatImageMessage( + topWidget: + message.text?.isNotEmpty == true + ? null + : TopWidget( + message, + groupStatus: groupStatus, + alwaysShow: true, + ), customImageProvider: CachedNetworkImage( message.source, ref.watch( diff --git a/lib/widgets/chat_page/top_widget.dart b/lib/widgets/chat_page/top_widget.dart index ae7baa6..b8ee5db 100644 --- a/lib/widgets/chat_page/top_widget.dart +++ b/lib/widgets/chat_page/top_widget.dart @@ -22,11 +22,13 @@ class TopWidget extends ConsumerWidget { children: [ Builder( builder: (_) { - final replyMessage = message.metadata?["reply"] as TextMessage?; + final replyMessage = message.metadata?["reply"] as Message?; if (replyMessage == null) return SizedBox.shrink(); - final smallerText = message is TextMessage - ? replyMessage.text.substring( + + final smallerText = + message is TextMessage && replyMessage.metadata!["body"] != null + ? replyMessage.metadata!["body"].substring( 0, min( max( @@ -39,14 +41,14 @@ class TopWidget extends ConsumerWidget { ), 5, ), - replyMessage.text.length, + replyMessage.metadata!["body"].length, ), ) : null; final replyText = (smallerText == null || - smallerText.length == replyMessage.text.length) - ? replyMessage.text + smallerText.length == replyMessage.metadata!["body"].length) + ? replyMessage.metadata!["body"] : "$smallerText..."; return Padding(