From 529a2e35c49f78d868b4fb19ddcd6537cdb10133 Mon Sep 17 00:00:00 2001 From: Henry-Hiles Date: Thu, 24 Sep 2026 14:41:13 -0400 Subject: [PATCH] make buildEventOptions an extension --- lib/helpers/build_event_options.dart | 256 ----------------- .../extensions/build_event_options.dart | 264 ++++++++++++++++++ lib/widgets/room_chat/room_chat.dart | 26 +- 3 files changed, 277 insertions(+), 269 deletions(-) delete mode 100644 lib/helpers/build_event_options.dart create mode 100644 lib/helpers/extensions/build_event_options.dart diff --git a/lib/helpers/build_event_options.dart b/lib/helpers/build_event_options.dart deleted file mode 100644 index b713351..0000000 --- a/lib/helpers/build_event_options.dart +++ /dev/null @@ -1,256 +0,0 @@ -import "package:fast_immutable_collections/fast_immutable_collections.dart"; -import "package:material_ui/material_ui.dart"; -import "package:flutter/services.dart"; -import "package:flutter_hooks/flutter_hooks.dart"; -import "package:hooks_riverpod/hooks_riverpod.dart"; -import "package:nexus/controllers/client.dart"; -import "package:nexus/controllers/pinned_ids.dart"; -import "package:nexus/controllers/power_level.dart"; -import "package:nexus/controllers/recent_emoji.dart"; -import "package:nexus/controllers/rooms.dart"; -import "package:nexus/controllers/room_chat.dart"; -import "package:nexus/controllers/via.dart"; -import "package:nexus/models/content/message.dart"; -import "package:nexus/models/event.dart"; -import "package:nexus/models/relation_type.dart"; -import "package:nexus/widgets/emoji_picker.dart"; -import "package:nexus/main.dart"; - -IList buildEventOptions({ - required BuildContext context, - required WidgetRef ref, - required String roomId, - required String userId, - required Event event, - required void Function(Event, RelationType) onRelation, -}) { - final theme = Theme.of(context); - final danger = theme.colorScheme.error; - - final notifier = ref.read(RoomChatController.provider(roomId).notifier); - final client = ref.read(ClientController.provider.notifier); - - final isPinned = ref - .watch(PinnedIdsController.provider(roomId)) - .contains(event.eventId); - - Future sendReaction(String emoji) async { - await notifier.sendReaction(emoji, event).onError(showError); - - await ref - .read(RecentEmojiController.provider.notifier) - .add(emoji) - .onError(showError); - } - - void showReasonDialog({ - required String title, - required String description, - required String action, - required Future Function(String reason) onConfirm, - }) { - showDialog( - context: context, - builder: (context) => HookBuilder( - builder: (context) { - final reasonController = useTextEditingController(); - - return AlertDialog( - title: Text(title), - content: Column( - mainAxisSize: .min, - crossAxisAlignment: .start, - children: [ - Text(description), - const SizedBox(height: 12), - TextField( - controller: reasonController, - textCapitalization: .sentences, - decoration: .new(labelText: "Reason (optional)"), - ), - ], - ), - actions: [ - TextButton( - onPressed: Navigator.of(context).pop, - child: const Text("Cancel"), - ), - TextButton( - onPressed: () { - final reason = reasonController.text; - Navigator.of(context).pop(); - - onConfirm(reason).onError(showError); - }, - child: Text(action), - ), - ], - ); - }, - ), - ); - } - - return .new([ - if (ref.watch( - PowerLevelController.provider(.new(eventType: .reaction, roomId: roomId)), - )) - PopupMenuItem( - enabled: false, - child: IconTheme( - data: theme.iconTheme, - child: Row( - children: [ - for (final emoji in { - ...ref - .watch(RecentEmojiController.provider) - .map((entry) => entry.emoji), - "👍", - "🤣", - "😭", - "🤔", - }.take(4)) - IconButton( - icon: Text(emoji), - onPressed: () { - Navigator.of(context).pop(); - sendReaction(emoji); - }, - ), - IconButton( - icon: const Icon(Icons.emoji_emotions), - onPressed: () { - Navigator.of(context).pop(); - - showModalBottomSheet( - context: context, - isScrollControlled: true, - builder: (context) => EmojiPicker( - allowFreeText: true, - onSelection: (emoji) { - Navigator.of(context).pop(); - sendReaction(emoji); - }, - ), - ); - }, - ), - ], - ), - ), - ), - - if (ref.watch( - PowerLevelController.provider(.new(eventType: .message, roomId: roomId)), - )) - PopupMenuItem( - onTap: () => onRelation(event, .reply), - child: const ListTile(leading: Icon(Icons.reply), title: Text("Reply")), - ), - - if (event.content is MessageContent && event.sender == userId) - PopupMenuItem( - onTap: () => onRelation(event, .edit), - child: const ListTile(leading: Icon(Icons.edit), title: Text("Edit")), - ), - - if (ref.watch( - PowerLevelController.provider( - .state(eventType: .pinnedEvents, roomId: roomId), - ), - )) - PopupMenuItem( - onTap: () async { - try { - final pins = ref.read( - PinnedIdsController.provider(roomId).notifier, - ); - - if (isPinned) { - await pins.removePin(event.eventId); - } else { - await pins.addPin(event.eventId); - } - } catch (error, stackTrace) { - showError(error, stackTrace); - } - }, - child: ListTile( - leading: const Icon(Icons.push_pin), - title: Text(isPinned ? "Unpin Event" : "Pin Event"), - ), - ), - - PopupMenuItem( - onTap: () async { - final room = ref.read( - RoomsController.provider.select((rooms) => rooms[roomId]), - ); - if (room == null) return; - - final vias = ref.read(ViaController.provider(room)); - - await Clipboard.setData( - ClipboardData( - text: - "matrix:roomid/${room.metadata?.id.substring(1)}/e/${event.eventId}$vias", - ), - ); - }, - child: const ListTile( - leading: Icon(Icons.link), - title: Text("Copy Link"), - ), - ), - - if (event.content case MessageContent(:final body?)) - PopupMenuItem( - onTap: () => Clipboard.setData(ClipboardData(text: body)), - child: const ListTile( - leading: Icon(Icons.copy), - title: Text("Copy Text"), - ), - ), - - if (ref.watch( - PowerLevelController.provider( - .redaction(targetUser: event.sender, roomId: roomId), - ), - )) - PopupMenuItem( - onTap: () => showReasonDialog( - title: "Delete Message", - description: - "Are you sure you want to delete this message? " - "This cannot be reversed.", - action: "Delete", - onConfirm: (reason) => notifier.deleteMessage(event, reason: reason), - ), - child: ListTile( - leading: Icon(Icons.delete, color: danger), - title: Text("Delete", style: .new(color: danger)), - ), - ), - - PopupMenuItem( - onTap: () => showReasonDialog( - title: "Report", - description: - "Report this event to your server administrators, " - "who can take action like banning this server or room.", - action: "Report", - onConfirm: (reason) => client.reportEvent( - .new( - roomId: roomId, - eventId: event.eventId, - reason: reason.isEmpty ? null : reason, - ), - ), - ), - child: ListTile( - leading: Icon(Icons.report, color: danger), - title: Text("Report", style: .new(color: danger)), - ), - ), - ]); -} diff --git a/lib/helpers/extensions/build_event_options.dart b/lib/helpers/extensions/build_event_options.dart new file mode 100644 index 0000000..5319f2b --- /dev/null +++ b/lib/helpers/extensions/build_event_options.dart @@ -0,0 +1,264 @@ +import "package:fast_immutable_collections/fast_immutable_collections.dart"; +import "package:material_ui/material_ui.dart"; +import "package:flutter/services.dart"; +import "package:flutter_hooks/flutter_hooks.dart"; +import "package:hooks_riverpod/hooks_riverpod.dart"; +import "package:nexus/controllers/client.dart"; +import "package:nexus/controllers/pinned_ids.dart"; +import "package:nexus/controllers/power_level.dart"; +import "package:nexus/controllers/recent_emoji.dart"; +import "package:nexus/controllers/rooms.dart"; +import "package:nexus/controllers/room_chat.dart"; +import "package:nexus/controllers/via.dart"; +import "package:nexus/models/content/message.dart"; +import "package:nexus/models/event.dart"; +import "package:nexus/models/relation_type.dart"; +import "package:nexus/widgets/emoji_picker.dart"; +import "package:nexus/main.dart"; + +extension BuildEventOptions on Event { + IList buildEventOptions({ + required BuildContext context, + required WidgetRef ref, + required String roomId, + required String userId, + required void Function(Event, RelationType) onRelation, + }) { + final theme = Theme.of(context); + final danger = theme.colorScheme.error; + + final notifier = ref.read(RoomChatController.provider(roomId).notifier); + final client = ref.read(ClientController.provider.notifier); + + final isPinned = ref + .watch(PinnedIdsController.provider(roomId)) + .contains(eventId); + + Future sendReaction(String emoji) async { + await notifier.sendReaction(emoji, this).onError(showError); + + await ref + .read(RecentEmojiController.provider.notifier) + .add(emoji) + .onError(showError); + } + + void showReasonDialog({ + required String title, + required String description, + required String action, + required Future Function(String reason) onConfirm, + }) { + showDialog( + context: context, + builder: (context) => HookBuilder( + builder: (context) { + final reasonController = useTextEditingController(); + + return AlertDialog( + title: Text(title), + content: Column( + mainAxisSize: .min, + crossAxisAlignment: .start, + children: [ + Text(description), + const SizedBox(height: 12), + TextField( + controller: reasonController, + textCapitalization: .sentences, + decoration: .new(labelText: "Reason (optional)"), + ), + ], + ), + actions: [ + TextButton( + onPressed: Navigator.of(context).pop, + child: const Text("Cancel"), + ), + TextButton( + onPressed: () { + final reason = reasonController.text; + Navigator.of(context).pop(); + + onConfirm(reason).onError(showError); + }, + child: Text(action), + ), + ], + ); + }, + ), + ); + } + + return .new([ + if (ref.watch( + PowerLevelController.provider( + .new(eventType: .reaction, roomId: roomId), + ), + )) + PopupMenuItem( + enabled: false, + child: IconTheme( + data: theme.iconTheme, + child: Row( + children: [ + for (final emoji in { + ...ref + .watch(RecentEmojiController.provider) + .map((entry) => entry.emoji), + "👍", + "🤣", + "😭", + "🤔", + }.take(4)) + IconButton( + icon: Text(emoji), + onPressed: () { + Navigator.of(context).pop(); + sendReaction(emoji); + }, + ), + IconButton( + icon: const Icon(Icons.emoji_emotions), + onPressed: () { + Navigator.of(context).pop(); + + showModalBottomSheet( + context: context, + isScrollControlled: true, + builder: (context) => EmojiPicker( + allowFreeText: true, + onSelection: (emoji) { + Navigator.of(context).pop(); + sendReaction(emoji); + }, + ), + ); + }, + ), + ], + ), + ), + ), + + if (ref.watch( + PowerLevelController.provider( + .new(eventType: .message, roomId: roomId), + ), + )) + PopupMenuItem( + onTap: () => onRelation(this, .reply), + child: const ListTile( + leading: Icon(Icons.reply), + title: Text("Reply"), + ), + ), + + if (content is MessageContent && sender == userId) + PopupMenuItem( + onTap: () => onRelation(this, .edit), + child: const ListTile(leading: Icon(Icons.edit), title: Text("Edit")), + ), + + if (ref.watch( + PowerLevelController.provider( + .state(eventType: .pinnedEvents, roomId: roomId), + ), + )) + PopupMenuItem( + onTap: () async { + try { + final pins = ref.read( + PinnedIdsController.provider(roomId).notifier, + ); + + if (isPinned) { + await pins.removePin(eventId); + } else { + await pins.addPin(eventId); + } + } catch (error, stackTrace) { + showError(error, stackTrace); + } + }, + child: ListTile( + leading: const Icon(Icons.push_pin), + title: Text(isPinned ? "Unpin Event" : "Pin Event"), + ), + ), + + PopupMenuItem( + onTap: () async { + final room = ref.read( + RoomsController.provider.select((rooms) => rooms[roomId]), + ); + if (room == null) return; + + final vias = ref.read(ViaController.provider(room)); + + await Clipboard.setData( + ClipboardData( + text: + "matrix:roomid/${room.metadata?.id.substring(1)}/e/$eventId$vias", + ), + ); + }, + child: const ListTile( + leading: Icon(Icons.link), + title: Text("Copy Link"), + ), + ), + + if (content case MessageContent(:final body?)) + PopupMenuItem( + onTap: () => Clipboard.setData(ClipboardData(text: body)), + child: const ListTile( + leading: Icon(Icons.copy), + title: Text("Copy Text"), + ), + ), + + if (ref.watch( + PowerLevelController.provider( + .redaction(targetUser: sender, roomId: roomId), + ), + )) + PopupMenuItem( + onTap: () => showReasonDialog( + title: "Delete Message", + description: + "Are you sure you want to delete this message? " + "This cannot be reversed.", + action: "Delete", + onConfirm: (reason) => notifier.deleteMessage(this, reason: reason), + ), + child: ListTile( + leading: Icon(Icons.delete, color: danger), + title: Text("Delete", style: .new(color: danger)), + ), + ), + + PopupMenuItem( + onTap: () => showReasonDialog( + title: "Report", + description: + "Report this this to your server administrators, " + "who can take action like banning this server or room.", + action: "Report", + onConfirm: (reason) => client.reportEvent( + .new( + roomId: roomId, + eventId: eventId, + reason: reason.isEmpty ? null : reason, + ), + ), + ), + child: ListTile( + leading: Icon(Icons.report, color: danger), + title: Text("Report", style: .new(color: danger)), + ), + ), + ]); + } +} diff --git a/lib/widgets/room_chat/room_chat.dart b/lib/widgets/room_chat/room_chat.dart index d6e8325..df3cf56 100644 --- a/lib/widgets/room_chat/room_chat.dart +++ b/lib/widgets/room_chat/room_chat.dart @@ -18,7 +18,7 @@ import "package:nexus/widgets/room_appbar.dart"; import "package:nexus/main.dart"; import "package:super_sliver_list/super_sliver_list.dart"; import "package:nexus/widgets/room_chat/chat_timeline.dart"; -import "package:nexus/helpers/build_event_options.dart"; +import "package:nexus/helpers/extensions/build_event_options.dart"; final class const RoomChat({ required final String? roomId, @@ -193,18 +193,18 @@ final class const RoomChat({ }, ); - IList getEventOptions(Event event) => buildEventOptions( - context: context, - ref: ref, - roomId: roomId, - userId: userId, - event: event, - onRelation: (event, type) { - relatedEvent.value = event; - relationType.value = type; - composerNode.requestFocus(); - }, - ); + IList getEventOptions(Event event) => + event.buildEventOptions( + context: context, + ref: ref, + roomId: roomId, + userId: userId, + onRelation: (event, type) { + relatedEvent.value = event; + relationType.value = type; + composerNode.requestFocus(); + }, + ); return Scaffold( endDrawer: PinnedEventsDrawer(