From 9663995114261c708f35c0439021d2aed26caf2a Mon Sep 17 00:00:00 2001 From: Henry-Hiles Date: Sat, 27 Dec 2025 23:42:30 -0500 Subject: [PATCH] fix newlines, wip edits --- lib/controllers/room_chat_controller.dart | 14 ++++-- lib/helpers/extensions/event_to_message.dart | 6 ++- lib/models/relation_type.dart | 1 + lib/widgets/chat_page/chat_box.dart | 27 +++++++++--- ...ply_preview.dart => relation_preview.dart} | 43 +++++++++++-------- lib/widgets/chat_page/room_chat.dart | 21 ++++++--- 6 files changed, 79 insertions(+), 33 deletions(-) create mode 100644 lib/models/relation_type.dart rename lib/widgets/chat_page/{reply_preview.dart => relation_preview.dart} (56%) diff --git a/lib/controllers/room_chat_controller.dart b/lib/controllers/room_chat_controller.dart index 5ce3584..1a1be39 100644 --- a/lib/controllers/room_chat_controller.dart +++ b/lib/controllers/room_chat_controller.dart @@ -8,6 +8,7 @@ import "package:nexus/controllers/events_controller.dart"; import "package:nexus/helpers/extensions/event_to_message.dart"; import "package:nexus/helpers/extensions/list_to_messages.dart"; import "package:fluttertagger/fluttertagger.dart" as tagger; +import "package:nexus/models/relation_type.dart"; class RoomChatController extends AsyncNotifier { final Room room; @@ -37,7 +38,10 @@ class RoomChatController extends AsyncNotifier { (element) => element.id == event.relationshipEventId, ); if (oldMessage == null || message == null) return; - return await updateMessage(oldMessage, message); + return await updateMessage( + oldMessage, + message.copyWith(id: oldMessage.id), + ); } if (message != null) { return await insertMessage(message); @@ -97,7 +101,8 @@ class RoomChatController extends AsyncNotifier { Future send( String message, { required Iterable tags, - Message? replyTo, + required RelationType relationType, + Message? relation, }) async { var taggedMessage = message; @@ -113,7 +118,10 @@ class RoomChatController extends AsyncNotifier { await room.sendTextEvent( taggedMessage, - inReplyTo: replyTo == null ? null : await room.getEventById(replyTo.id), + editEventId: relationType == RelationType.edit ? relation?.id : null, + inReplyTo: (relationType == RelationType.reply && relation != null) + ? await room.getEventById(relation.id) + : null, ); } diff --git a/lib/helpers/extensions/event_to_message.dart b/lib/helpers/extensions/event_to_message.dart index 22e4988..c003180 100644 --- a/lib/helpers/extensions/event_to_message.dart +++ b/lib/helpers/extensions/event_to_message.dart @@ -25,8 +25,10 @@ extension EventToMessage on Event { newContent?["formatted_body"] ?? newContent?["body"] ?? event.content["formatted_body"] ?? - event.content["body"], + event.content["body"] ?? + "", "reply": await replyEvent?.toMessage(mustBeText: true), + "body": newContent?["body"] ?? event.content["body"], "eventType": event.type, "avatarUrl": sender.avatarUrl.toString(), "displayName": sender.displayName ?? sender.id, @@ -69,7 +71,7 @@ extension EventToMessage on Event { return switch (type) { EventTypes.Encrypted => asText.copyWith( text: "Unable to decrypt message.", - metadata: {"formatted": "Unable to decrypt message.", ...metadata}, + metadata: {...metadata, "formatted": "Unable to decrypt message."}, ), (EventTypes.Sticker || EventTypes.Message) => switch (messageType) { (MessageTypes.Sticker || MessageTypes.Image) => Message.image( diff --git a/lib/models/relation_type.dart b/lib/models/relation_type.dart new file mode 100644 index 0000000..80c5223 --- /dev/null +++ b/lib/models/relation_type.dart @@ -0,0 +1 @@ +enum RelationType { edit, reply } diff --git a/lib/widgets/chat_page/chat_box.dart b/lib/widgets/chat_page/chat_box.dart index 92c44fc..016e3a7 100644 --- a/lib/widgets/chat_page/chat_box.dart +++ b/lib/widgets/chat_page/chat_box.dart @@ -7,15 +7,18 @@ import "package:fluttertagger/fluttertagger.dart"; import "package:hooks_riverpod/hooks_riverpod.dart"; import "package:matrix/matrix.dart"; import "package:nexus/controllers/room_chat_controller.dart"; +import "package:nexus/models/relation_type.dart"; import "package:nexus/widgets/chat_page/mention_overlay.dart"; -import "package:nexus/widgets/chat_page/reply_preview.dart"; +import "package:nexus/widgets/chat_page/relation_preview.dart"; class ChatBox extends HookConsumerWidget { - final Message? replyToMessage; + final Message? relatedMessage; + final RelationType relationType; final VoidCallback onDismiss; final Room room; const ChatBox({ - required this.replyToMessage, + required this.relatedMessage, + required this.relationType, required this.onDismiss, required this.room, super.key, @@ -28,14 +31,25 @@ class ChatBox extends HookConsumerWidget { final triggerCharacter = useState(""); final query = useState(""); + if (relationType == RelationType.edit && + relatedMessage is TextMessage && + controller.value.text.isEmpty) { + final text = (relatedMessage as TextMessage).text; + controller.value.text = relatedMessage?.replyToMessageId == null + ? text + : text.split("\n\n").sublist(1).join("\n\n"); + } + void send() { ref .watch(RoomChatController.provider(room).notifier) .send( controller.value.formattedText, - replyTo: replyToMessage, + relation: relatedMessage, + relationType: relationType, tags: controller.value.tags, ); + onDismiss(); controller.value.text = ""; } @@ -71,8 +85,9 @@ class ChatBox extends HookConsumerWidget { borderRadius: BorderRadius.all(Radius.circular(12)), child: Column( children: [ - ReplyPreview( - replyToMessage: replyToMessage, + RelationPreview( + relatedMessage: relatedMessage, + relationType: relationType, onDismiss: onDismiss, room: room, ), diff --git a/lib/widgets/chat_page/reply_preview.dart b/lib/widgets/chat_page/relation_preview.dart similarity index 56% rename from lib/widgets/chat_page/reply_preview.dart rename to lib/widgets/chat_page/relation_preview.dart index 4f1c1eb..07bac4e 100644 --- a/lib/widgets/chat_page/reply_preview.dart +++ b/lib/widgets/chat_page/relation_preview.dart @@ -4,14 +4,17 @@ import "package:hooks_riverpod/hooks_riverpod.dart"; import "package:matrix/matrix.dart"; import "package:nexus/controllers/avatar_controller.dart"; import "package:nexus/helpers/extensions/get_headers.dart"; +import "package:nexus/models/relation_type.dart"; import "package:nexus/widgets/avatar_or_hash.dart"; -class ReplyPreview extends ConsumerWidget { - final Message? replyToMessage; +class RelationPreview extends ConsumerWidget { + final Message? relatedMessage; + final RelationType relationType; final VoidCallback onDismiss; final Room room; - const ReplyPreview({ - required this.replyToMessage, + const RelationPreview({ + required this.relatedMessage, + required this.relationType, required this.onDismiss, required this.room, super.key, @@ -19,9 +22,9 @@ class ReplyPreview extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { + if (relatedMessage == null) return SizedBox.shrink(); final theme = Theme.of(context); - if (replyToMessage == null) return SizedBox.shrink(); return Container( color: theme.colorScheme.surfaceContainerHigh, padding: EdgeInsets.symmetric(horizontal: 8), @@ -29,34 +32,40 @@ class ReplyPreview extends ConsumerWidget { spacing: 8, children: [ SizedBox(width: 4), + if (relationType == RelationType.edit) + Text( + "Editing message:", + style: TextStyle(fontWeight: FontWeight.bold), + ), AvatarOrHash( ref .watch( AvatarController.provider( - replyToMessage!.metadata!["avatarUrl"], + relatedMessage!.metadata!["avatarUrl"], ), ) .whenOrNull(data: (data) => data), - replyToMessage!.metadata!["displayName"].toString(), + relatedMessage!.metadata!["displayName"].toString(), headers: room.client.headers, height: 16, ), Text( - replyToMessage!.metadata?["displayName"] ?? - replyToMessage!.authorId, + relatedMessage!.metadata?["displayName"] ?? + relatedMessage!.authorId, style: theme.textTheme.labelMedium?.copyWith( fontWeight: FontWeight.bold, ), ), Expanded( - child: (replyToMessage is TextMessage) - ? Text( - (replyToMessage as TextMessage).text, - overflow: TextOverflow.ellipsis, - style: theme.textTheme.labelMedium, - maxLines: 1, - ) - : SizedBox(), + child: Text( + (relatedMessage is TextMessage) + ? (relatedMessage as TextMessage).text + : relatedMessage?.metadata?["body"] ?? + relatedMessage?.metadata?["eventType"], + overflow: TextOverflow.ellipsis, + style: theme.textTheme.labelMedium, + maxLines: 1, + ), ), IconButton( onPressed: onDismiss, diff --git a/lib/widgets/chat_page/room_chat.dart b/lib/widgets/chat_page/room_chat.dart index a95af30..0190e64 100644 --- a/lib/widgets/chat_page/room_chat.dart +++ b/lib/widgets/chat_page/room_chat.dart @@ -13,6 +13,7 @@ import "package:nexus/controllers/room_chat_controller.dart"; import "package:nexus/helpers/extensions/better_when.dart"; import "package:nexus/helpers/extensions/get_headers.dart"; import "package:nexus/helpers/extensions/show_context_menu.dart"; +import "package:nexus/models/relation_type.dart"; import "package:nexus/widgets/chat_page/chat_box.dart"; import "package:nexus/widgets/chat_page/html/html.dart"; import "package:nexus/widgets/chat_page/member_list.dart"; @@ -34,6 +35,7 @@ class RoomChat extends HookConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final replyToMessage = useState(null); final memberListOpened = useState(showMembersByDefault); + final relationType = useState(RelationType.reply); final theme = Theme.of(context); final danger = theme.colorScheme.error; @@ -56,7 +58,10 @@ class RoomChat extends HookConsumerWidget { List getMessageOptions(Message message) => [ PopupMenuItem( - onTap: () => replyToMessage.value = message, + onTap: () { + replyToMessage.value = message; + relationType.value = RelationType.reply; + }, child: ListTile( leading: Icon(Icons.reply), title: Text("Reply"), @@ -64,7 +69,10 @@ class RoomChat extends HookConsumerWidget { ), if (message.authorId == room.roomData.client.userID) PopupMenuItem( - onTap: () {}, + onTap: () { + replyToMessage.value = message; + relationType.value = RelationType.edit; + }, child: ListTile( leading: Icon(Icons.edit), title: Text("Edit"), @@ -217,7 +225,8 @@ class RoomChat extends HookConsumerWidget { bottomPadding: 72, ), composerBuilder: (_) => ChatBox( - replyToMessage: replyToMessage.value, + relationType: relationType.value, + relatedMessage: replyToMessage.value, onDismiss: () => replyToMessage.value = null, room: room.roomData, @@ -231,7 +240,8 @@ class RoomChat extends HookConsumerWidget { MessageGroupStatus? groupStatus, }) => FlyerChatTextMessage( customWidget: Html( - message.metadata?["formatted"] + (message.metadata?["formatted"] + as String) .replaceAllMapped( RegExp( regexLink, @@ -239,7 +249,8 @@ class RoomChat extends HookConsumerWidget { ), (m) => "${m.group(0)!}", - ) + + ) + .replaceAll("\n", "
") + ((message.editedAt != null) ? "(edited)" : ""),