From 1d03b097751a6b8aab4fb4067482eaf1bdb08d4f Mon Sep 17 00:00:00 2001 From: Henry-Hiles Date: Wed, 12 Nov 2025 14:33:37 -0500 Subject: [PATCH] dedupe replies --- lib/controllers/room_chat_controller.dart | 8 +- lib/helpers/extension_helper.dart | 11 ++- lib/main.dart | 4 +- lib/widgets/room_chat.dart | 92 +++++++++++++++-------- lib/widgets/top_widget.dart | 1 + pubspec.lock | 10 ++- pubspec.yaml | 1 + 7 files changed, 86 insertions(+), 41 deletions(-) diff --git a/lib/controllers/room_chat_controller.dart b/lib/controllers/room_chat_controller.dart index f0b38c5..c4a819d 100644 --- a/lib/controllers/room_chat_controller.dart +++ b/lib/controllers/room_chat_controller.dart @@ -43,9 +43,11 @@ class RoomChatController extends AsyncNotifier { return controller.updateMessage(message, newMessage); } - Future send(String message) async { - await room.sendTextEvent(message); - } + Future send(String message, {String? replyTo}) async => + await room.sendTextEvent( + message, + inReplyTo: replyTo == null ? null : await room.getEventById(replyTo), + ); Future resolveUser(String id) async { final user = await room.client.getUserProfile(id); diff --git a/lib/helpers/extension_helper.dart b/lib/helpers/extension_helper.dart index ebe62af..edd041b 100644 --- a/lib/helpers/extension_helper.dart +++ b/lib/helpers/extension_helper.dart @@ -5,6 +5,7 @@ import "package:matrix/matrix.dart"; import "package:nexus/models/full_room.dart"; import "package:nexus/widgets/error_dialog.dart"; import "package:nexus/widgets/loading.dart"; +import "package:html2md/html2md.dart"; extension BetterWhen on AsyncValue { Widget betterWhen({ @@ -60,11 +61,16 @@ extension ToMessage on Event { ); } + final formatted = convert( + formattedText.isEmpty ? body : formattedText, + ignore: replyId == null ? null : ["mx-reply"], + ); + final asText = Message.text( metadata: metadata, id: eventId, authorId: senderId, - text: body, + text: formatted, replyToMessageId: replyId, deliveredAt: originServerTs, ); @@ -77,6 +83,7 @@ extension ToMessage on Event { metadata: metadata, id: eventId, authorId: senderId, + text: formatted, source: (await getAttachmentUri()).toString(), replyToMessageId: replyId, deliveredAt: originServerTs, @@ -85,7 +92,7 @@ extension ToMessage on Event { metadata: metadata, id: eventId, authorId: senderId, - text: body, + text: formatted, replyToMessageId: replyId, source: (await getAttachmentUri()).toString(), deliveredAt: originServerTs, diff --git a/lib/main.dart b/lib/main.dart index adb288d..6fb7d11 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -8,9 +8,7 @@ import "package:dynamic_system_colors/dynamic_system_colors.dart"; import "package:window_size/window_size.dart"; void main() async { - ScaledWidgetsFlutterBinding.ensureInitialized( - scaleFactor: (size) => size.width > 1080 ? 1.3 : 1, - ); + ScaledWidgetsFlutterBinding.ensureInitialized(scaleFactor: (size) => 1); await windowManager.ensureInitialized(); await windowManager.waitUntilReadyToShow( diff --git a/lib/widgets/room_chat.dart b/lib/widgets/room_chat.dart index 44002e5..4f701a0 100644 --- a/lib/widgets/room_chat.dart +++ b/lib/widgets/room_chat.dart @@ -4,6 +4,7 @@ import "package:flutter/foundation.dart"; import "package:flutter/material.dart"; import "package:flutter_chat_core/flutter_chat_core.dart"; import "package:flutter_chat_ui/flutter_chat_ui.dart"; +import "package:flutter_hooks/flutter_hooks.dart"; import "package:flutter_link_previewer/flutter_link_previewer.dart"; import "package:flyer_chat_file_message/flyer_chat_file_message.dart"; import "package:flyer_chat_image_message/flyer_chat_image_message.dart"; @@ -21,8 +22,23 @@ class RoomChat extends HookConsumerWidget { final bool isDesktop; const RoomChat({required this.isDesktop, super.key}); + void showContextMenu({ + required BuildContext context, + required Offset globalPosition, + required VoidCallback onTap, + }) => showMenu( + context: context, + position: RelativeRect.fromRect( + Rect.fromPoints(globalPosition, globalPosition), + Offset.zero & (context.findRenderObject() as RenderBox).size, + ), + color: Theme.of(context).colorScheme.surfaceContainerHighest, + items: [PopupMenuItem(onTap: onTap, child: Text("Reply"))], + ); + @override Widget build(BuildContext context, WidgetRef ref) { + final replyToMessageId = useState(null); final urlRegex = RegExp(r"https?://[^\s\]\(\)]+"); final theme = Theme.of(context); return ref @@ -73,6 +89,28 @@ class RoomChat extends HookConsumerWidget { onPrimary: theme.colorScheme.onPrimaryContainer, ), ), + onMessageSecondaryTap: + ( + context, + message, { + required details, + required index, + }) => showContextMenu( + context: context, + globalPosition: details.globalPosition, + onTap: () => replyToMessageId.value = message.id, + ), + onMessageLongPress: + ( + context, + message, { + required details, + required index, + }) => showContextMenu( + context: context, + globalPosition: details.globalPosition, + onTap: () => replyToMessageId.value = message.id, + ), builders: Builders( composerBuilder: (_) => Composer( sendIconColor: theme.colorScheme.primary, @@ -101,36 +139,23 @@ class RoomChat extends HookConsumerWidget { index, { required bool isSentByMe, MessageGroupStatus? groupStatus, - }) => Column( - crossAxisAlignment: isSentByMe - ? CrossAxisAlignment.end - : CrossAxisAlignment.start, - spacing: 8, - children: [ - SizedBox(height: 8), - - FlyerChatTextMessage( - topWidget: TopWidget( - message, - headers: headers, - ), - message: message.copyWith( - text: message.text.replaceAllMapped( - urlRegex, - (match) => - "[${match.group(0)}](${match.group(0)})", - ), - ), - showTime: true, - index: index, - onLinkTap: (url, _) => ref - .watch(LaunchHelper.provider) - .launchUrl(Uri.parse(url)), - linksDecoration: TextDecoration.underline, - sentLinksColor: Colors.blue, - receivedLinksColor: Colors.blue, + }) => FlyerChatTextMessage( + topWidget: TopWidget(message, headers: headers), + message: message.copyWith( + text: message.text.replaceAllMapped( + urlRegex, + (match) => + "[${match.group(0)}](${match.group(0)})", ), - ], + ), + showTime: true, + index: index, + onLinkTap: (url, _) => ref + .watch(LaunchHelper.provider) + .launchUrl(Uri.parse(url)), + linksDecoration: TextDecoration.underline, + sentLinksColor: Colors.blue, + receivedLinksColor: Colors.blue, ), linkPreviewBuilder: (_, message, isSentByMe) => LinkPreview( @@ -196,9 +221,12 @@ class RoomChat extends HookConsumerWidget { index: index, ), ), - onMessageSend: ref - .watch(controllerProvider.notifier) - .send, + onMessageSend: (message) { + ref + .watch(controllerProvider.notifier) + .send(message, replyTo: replyToMessageId.value); + replyToMessageId.value = null; + }, resolveUser: ref .watch(controllerProvider.notifier) .resolveUser, diff --git a/lib/widgets/top_widget.dart b/lib/widgets/top_widget.dart index 582755e..623b2aa 100644 --- a/lib/widgets/top_widget.dart +++ b/lib/widgets/top_widget.dart @@ -73,6 +73,7 @@ class TopWidget extends ConsumerWidget { replyText, overflow: TextOverflow.ellipsis, style: Theme.of(context).textTheme.labelMedium, + maxLines: 1, ), ), ], diff --git a/pubspec.lock b/pubspec.lock index 85de9e3..5900f42 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -491,7 +491,7 @@ packages: description: path: "packages/flutter_chat_ui" ref: HEAD - resolved-ref: c1ef794e78e56308872ec377c91645a483204a02 + resolved-ref: f6718923519db812762ff27eb402f70076d8676c url: "https://github.com/Henry-Hiles/flutter_chat_ui" source: git version: "2.9.1" @@ -694,6 +694,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.15.6" + html2md: + dependency: "direct main" + description: + name: html2md + sha256: "465cf8ffa1b510fe0e97941579bf5b22e2d575f2cecb500a9c0254efe33a8036" + url: "https://pub.dev" + source: hosted + version: "1.3.2" html_unescape: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index da107c7..0316d23 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -53,6 +53,7 @@ dependencies: sqflite_common_ffi: ^2.3.6 color_hash: ^1.0.1 scaled_app: ^2.3.0 + html2md: ^1.3.2 dev_dependencies: build_runner: ^2.4.11