From 789fa6efbd4ce72fa729e186d0826e45a0fd7058 Mon Sep 17 00:00:00 2001 From: Steven Lageveen Date: Sun, 12 Oct 2025 23:40:01 +0200 Subject: [PATCH] Add quickactions for chats --- lib/pages/chat/chat.dart | 98 ++++++++++++++++++- lib/pages/chat/chat_event_list.dart | 5 +- lib/pages/chat/chat_view.dart | 2 +- lib/pages/chat/events/message.dart | 51 +++++++++- .../local_notifications_extension.dart | 3 +- lib/widgets/matrix.dart | 2 - 6 files changed, 152 insertions(+), 9 deletions(-) diff --git a/lib/pages/chat/chat.dart b/lib/pages/chat/chat.dart index c415b98c8..8d4d5605d 100644 --- a/lib/pages/chat/chat.dart +++ b/lib/pages/chat/chat.dart @@ -835,6 +835,53 @@ class ChatController extends State } } + void redactEventAction(Event event) async { + final reasonInput = event.status.isSent + ? await showTextInputDialog( + context: context, + title: L10n.of(context).redactMessage, + message: L10n.of(context).redactMessageDescription, + isDestructive: true, + hintText: L10n.of(context).optionalRedactReason, + maxLength: 255, + maxLines: 3, + minLines: 1, + okLabel: L10n.of(context).remove, + cancelLabel: L10n.of(context).cancel, + ) + : null; + if (reasonInput == null) return; + final reason = reasonInput.isEmpty ? null : reasonInput; + await showFutureLoadingDialog( + context: context, + futureWithProgress: (onProgress) async { + if (event.status.isSent) { + if (event.canRedact) { + await event.redactEvent(reason: reason); + } else { + final client = currentRoomBundle.firstWhere( + (cl) => selectedEvents.first.senderId == cl!.userID, + orElse: () => null, + ); + if (client == null) { + return; + } + final room = client.getRoomById(roomId)!; + await Event.fromJson(event.toJson(), room).redactEvent( + reason: reason, + ); + } + } else { + await event.cancelSend(); + } + }, + ); + setState(() { + showEmojiPicker = false; + selectedEvents.clear(); + }); + } + void redactEventsAction() async { final reasonInput = selectedEvents.any((event) => event.status.isSent) ? await showTextInputDialog( @@ -925,6 +972,20 @@ class ChatController extends State .any((cl) => selectedEvents.first.senderId == cl!.userID); } + void forwardEventAction(Event event) async { + final timeline = this.timeline; + if (timeline == null) return; + + final items = [ContentShareItem(event.getDisplayEvent(timeline).content)]; + await showScaffoldDialog( + context: context, + builder: (context) => ShareScaffoldDialog( + items: items, + ), + ); + if (!mounted) return; + } + void forwardEventsAction() async { if (selectedEvents.isEmpty) return; final timeline = this.timeline; @@ -1066,6 +1127,28 @@ class ChatController extends State } } + void editEventAction(Event event) { + final client = currentRoomBundle.firstWhere( + (cl) => event.senderId == cl!.userID, + orElse: () => null, + ); + if (client == null) { + return; + } + setSendingClient(client); + setState(() { + pendingText = sendController.text; + editEvent = event; + sendController.text = + editEvent!.getDisplayEvent(timeline!).calcLocalizedBodyFallback( + MatrixLocals(L10n.of(context)), + withSenderNamePrefix: false, + hideReply: true, + ); + }); + inputFocus.requestFocus(); + } + void editSelectedEventAction() { final client = currentRoomBundle.firstWhere( (cl) => selectedEvents.first.senderId == cl!.userID, @@ -1266,7 +1349,20 @@ class ChatController extends State } } - void pinEvent() { + void pinEvent(Event event) { + final pinnedEventIds = room.pinnedEventIds; + if (pinnedEventIds.contains(event.eventId)) { + pinnedEventIds.remove(event.eventId); + } else { + pinnedEventIds.add(event.eventId); + } + showFutureLoadingDialog( + context: context, + future: () => room.setPinnedEvents(pinnedEventIds), + ); + } + + void pinSelectedEvent() { final pinnedEventIds = room.pinnedEventIds; final selectedEventIds = selectedEvents.map((e) => e.eventId).toSet(); final unpin = selectedEventIds.length == 1 && diff --git a/lib/pages/chat/chat_event_list.dart b/lib/pages/chat/chat_event_list.dart index 2b2d45054..db3cf2b01 100644 --- a/lib/pages/chat/chat_event_list.dart +++ b/lib/pages/chat/chat_event_list.dart @@ -141,6 +141,10 @@ class ChatEventList extends StatelessWidget { controller.animateInEventIndex = null; }, onReply: () => controller.replyAction(replyTo: event), + onForward: () => controller.forwardEventAction(event), + onPin: () => controller.pinEvent(event), + onRedact: () => controller.redactEventAction(event), + onEdit: () => controller.editEventAction(event), onInfoTab: controller.showEventInfo, onMention: () => controller.sendController.text += '${event.senderFromMemoryOrFallback.mention} ', @@ -155,7 +159,6 @@ class ChatEventList extends StatelessWidget { singleSelected: controller.selectedEvents.singleOrNull?.eventId == event.eventId, - onEdit: () => controller.editSelectedEventAction(), timeline: timeline, displayReadMarker: i > 0 && controller.readMarkerEventId == event.eventId, diff --git a/lib/pages/chat/chat_view.dart b/lib/pages/chat/chat_view.dart index ae67cfc0d..eae77f1ee 100644 --- a/lib/pages/chat/chat_view.dart +++ b/lib/pages/chat/chat_view.dart @@ -53,7 +53,7 @@ class ChatView extends StatelessWidget { if (controller.canPinSelectedEvents) IconButton( icon: const Icon(Icons.push_pin_outlined), - onPressed: controller.pinEvent, + onPressed: controller.pinSelectedEvent, tooltip: L10n.of(context).pinMessage, ), if (controller.canRedactSelectedEvents) diff --git a/lib/pages/chat/events/message.dart b/lib/pages/chat/events/message.dart index f0a9241d3..28b04a93a 100644 --- a/lib/pages/chat/events/message.dart +++ b/lib/pages/chat/events/message.dart @@ -31,6 +31,9 @@ class Message extends StatelessWidget { final void Function(Event) onInfoTab; final void Function(String) scrollToEventId; final void Function() onReply; + final void Function() onForward; + final void Function() onPin; + final void Function() onRedact; final void Function() onMention; final void Function() onEdit; final bool longPressSelect; @@ -56,6 +59,9 @@ class Message extends StatelessWidget { required this.onInfoTab, required this.scrollToEventId, required this.onReply, + required this.onForward, + required this.onPin, + required this.onRedact, this.selected = false, required this.onEdit, required this.singleSelected, @@ -72,6 +78,37 @@ class Message extends StatelessWidget { super.key, }); + Future _showContextMenu(BuildContext context, Offset anchor) async { + final result = await showMenu( + context: context, + position: + RelativeRect.fromLTRB(anchor.dx, anchor.dy, anchor.dx, anchor.dy), + items: const [ + PopupMenuItem(value: 'reply', child: Text('Reply')), + PopupMenuItem(value: 'copy', child: Text('Copy')), + PopupMenuItem(value: 'forward', child: Text('Forward')), + PopupMenuItem(value: 'pin', child: Text('Pin')), + PopupMenuItem(value: 'edit', child: Text('Edit')), + PopupMenuItem(value: 'redact', child: Text('Redact')), + ], + ); + switch (result) { + case 'reply': + onReply(); + break; + case 'forward': + onForward(); + break; + case 'pin': + onPin(); + break; + case 'edit': + onEdit(); + break; + // handle others + } + } + @override Widget build(BuildContext context) { final theme = Theme.of(context); @@ -285,9 +322,17 @@ class Message extends StatelessWidget { : null, enableFeedback: !selected, onLongPress: () => onSelect(event), - onTap: longPressSelect - ? () => onSelect(event) - : null, + onTapUp: longPressSelect + ? (_) => onSelect(event) + : (details) => _showContextMenu( + context, + details.globalPosition, + ), + onSecondaryTapUp: (details) => + _showContextMenu( + context, + details.globalPosition, + ), borderRadius: BorderRadius.circular( AppConfig.borderRadius / 2, ), diff --git a/lib/widgets/local_notifications_extension.dart b/lib/widgets/local_notifications_extension.dart index 9f07092f4..058b65cdc 100644 --- a/lib/widgets/local_notifications_extension.dart +++ b/lib/widgets/local_notifications_extension.dart @@ -26,7 +26,8 @@ extension LocalNotificationsExtension on MatrixState { void showLocalNotification(Event event) async { Logs().v( - '[Notifications] event received for ${event.room.id} (${event.type})'); + '[Notifications] event received for ${event.room.id} (${event.type})', + ); final roomId = event.room.id; if (activeRoomId == roomId) { if (kIsWeb && webHasFocus) return; diff --git a/lib/widgets/matrix.dart b/lib/widgets/matrix.dart index a7995059f..b63a0eade 100644 --- a/lib/widgets/matrix.dart +++ b/lib/widgets/matrix.dart @@ -295,8 +295,6 @@ class MatrixState extends State with WidgetsBindingObserver { c.onNotification.stream.listen(showLocalNotification); }); } else if (PlatformInfos.isLinux || PlatformInfos.isMacOS) { - Logs().v( - '[Notifications] Subscribing desktop listener for ${c.clientName}'); onNotification[name] ??= c.onNotification.stream.listen(showLocalNotification); }