diff --git a/.vscode/settings.json b/.vscode/settings.json index 8708bf56..105b3214 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -2,6 +2,7 @@ "cSpell.words": [ "Appbar", "Displayname", + "fluttertagger", "Homeserver", "localpart", "prefs", diff --git a/lib/controllers/client_controller.dart b/lib/controllers/client_controller.dart index 6a06e10c..103a9d41 100644 --- a/lib/controllers/client_controller.dart +++ b/lib/controllers/client_controller.dart @@ -28,6 +28,7 @@ import "package:nexus/models/profile.dart"; import "package:nexus/models/requests/paginate_request.dart"; import "package:nexus/models/requests/redact_event_request.dart"; import "package:nexus/models/requests/report_request.dart"; +import "package:nexus/models/requests/send_event_request.dart"; import "package:nexus/models/requests/send_message_request.dart"; import "package:nexus/models/requests/set_membership_request.dart"; import "package:nexus/models/room.dart"; @@ -80,9 +81,13 @@ class ClientController extends AsyncNotifier { case "send_complete": final event = Event.fromJson(decodedMuksEvent["event"]); - ref - .watch(NewEventsController.provider(event.roomId).notifier) - .add(IList([event])); + if (event.type == "m.room.message") { + ref + .watch( + NewEventsController.provider(event.roomId).notifier, + ) + .add(IList([event])); + } break; case "sync_complete": final syncData = SyncData.fromJson(decodedMuksEvent); @@ -164,6 +169,9 @@ class ClientController extends AsyncNotifier { Future sendMessage(SendMessageRequest request) async => Event.fromJson(await _sendCommand("send_message", request.toJson())); + Future sendEvent(SendEventRequest request) async => + Event.fromJson(await _sendCommand("send_event", request.toJson())); + Future verify(String recoveryKey) async { try { await _sendCommand("verify", {"recovery_key": recoveryKey}); diff --git a/lib/controllers/room_chat_controller.dart b/lib/controllers/room_chat_controller.dart index 1ecf0ea7..aac51b2c 100644 --- a/lib/controllers/room_chat_controller.dart +++ b/lib/controllers/room_chat_controller.dart @@ -16,6 +16,7 @@ import "package:nexus/models/requests/get_room_state_request.dart"; import "package:nexus/models/requests/paginate_request.dart"; import "package:nexus/models/requests/redact_event_request.dart"; import "package:nexus/models/relation_type.dart"; +import "package:nexus/models/requests/send_event_request.dart"; import "package:nexus/models/requests/send_message_request.dart"; import "package:nexus/models/room.dart"; @@ -328,6 +329,25 @@ class RoomChatController extends AsyncNotifier { return await controller.scrollToMessage(message.id); } + Future sendReaction(String reaction, Message message) async { + final client = ref.watch(ClientController.provider.notifier); + + await client.sendEvent( + SendEventRequest( + roomId: roomId, + type: "m.reaction", + content: { + "m.relates_to": { + "event_id": message.id, + "rel_type": "m.annotation", + "key": reaction, + }, + }, + disableEncryption: true, + ), + ); + } + static final provider = AsyncNotifierProvider.family .autoDispose( RoomChatController.new, diff --git a/lib/models/requests/send_event_request.dart b/lib/models/requests/send_event_request.dart new file mode 100644 index 00000000..8b2d5c82 --- /dev/null +++ b/lib/models/requests/send_event_request.dart @@ -0,0 +1,16 @@ +import "package:freezed_annotation/freezed_annotation.dart"; +part "send_event_request.freezed.dart"; +part "send_event_request.g.dart"; + +@freezed +abstract class SendEventRequest with _$SendEventRequest { + const factory SendEventRequest({ + required String roomId, + required String type, + required Map content, + @Default(false) bool disableEncryption, + }) = _SendEventRequest; + + factory SendEventRequest.fromJson(Map json) => + _$SendEventRequestFromJson(json); +} diff --git a/lib/widgets/chat_page/wrappers/message_wrapper.dart b/lib/widgets/chat_page/wrappers/message_wrapper.dart index 75ed0379..9c70c27f 100644 --- a/lib/widgets/chat_page/wrappers/message_wrapper.dart +++ b/lib/widgets/chat_page/wrappers/message_wrapper.dart @@ -1,27 +1,20 @@ -import "package:cross_cache/cross_cache.dart"; -import "package:fast_immutable_collections/fast_immutable_collections.dart"; import "package:flutter/material.dart"; import "package:flutter_chat_core/flutter_chat_core.dart"; -import "package:flutter_riverpod/flutter_riverpod.dart"; -import "package:nexus/controllers/client_state_controller.dart"; -import "package:nexus/controllers/cross_cache_controller.dart"; -import "package:nexus/helpers/extensions/get_headers.dart"; -import "package:nexus/helpers/extensions/mxc_to_https.dart"; import "package:nexus/widgets/chat_page/lazy_loading/message_avatar.dart"; import "package:nexus/widgets/chat_page/lazy_loading/message_displayname.dart"; +import "package:nexus/widgets/chat_page/wrappers/reaction_row.dart"; import "package:timeago/timeago.dart"; -class MessageWrapper extends ConsumerWidget { +class MessageWrapper extends StatelessWidget { final Message message; final Widget child; final MessageGroupStatus? groupStatus; const MessageWrapper(this.message, this.child, this.groupStatus, {super.key}); @override - Widget build(BuildContext context, WidgetRef ref) { + Widget build(BuildContext context) { final theme = Theme.of(context); final error = message.metadata?["error"]; - final clientState = ref.watch(ClientStateController.provider); return ClipRRect( borderRadius: BorderRadius.all(Radius.circular(12)), @@ -78,53 +71,7 @@ class MessageWrapper extends ConsumerWidget { color: theme.colorScheme.error, ), ), - Wrap( - spacing: 4, - runSpacing: 4, - children: - clientState?.homeserverUrl == null || - message.reactions == null - ? [] - : message.reactions!.mapTo((reaction, reactors) { - final selected = reactors.contains( - clientState!.userId, - ); - return SizedBox( - child: Tooltip( - message: reactors.join(", "), - child: ChoiceChip( - showCheckmark: false, - selected: selected, - label: Row( - mainAxisSize: MainAxisSize.min, - spacing: 8, - children: [ - reaction.startsWith("mxc://") - ? Image( - height: 20, - image: CachedNetworkImage( - headers: ref.headers, - Uri.parse(reaction) - .mxcToHttps( - clientState - .homeserverUrl!, - ) - .toString(), - ref.watch( - CrossCacheController.provider, - ), - ), - ) - : Text(reaction), - Text(reactors.length.toString()), - ], - ), - onSelected: (value) {}, // TODO - ), - ), - ); - }).toList(), - ), + ReactionRow(message), ], ), ), diff --git a/lib/widgets/chat_page/wrappers/reaction_row.dart b/lib/widgets/chat_page/wrappers/reaction_row.dart new file mode 100644 index 00000000..1924cd2d --- /dev/null +++ b/lib/widgets/chat_page/wrappers/reaction_row.dart @@ -0,0 +1,77 @@ +import "package:cross_cache/cross_cache.dart"; +import "package:fast_immutable_collections/fast_immutable_collections.dart"; +import "package:flutter/material.dart"; +import "package:flutter_chat_core/flutter_chat_core.dart"; +import "package:flutter_riverpod/flutter_riverpod.dart"; +import "package:nexus/controllers/client_state_controller.dart"; +import "package:nexus/controllers/cross_cache_controller.dart"; +import "package:nexus/controllers/room_chat_controller.dart"; +import "package:nexus/controllers/selected_room_controller.dart"; +import "package:nexus/helpers/extensions/get_headers.dart"; +import "package:nexus/helpers/extensions/mxc_to_https.dart"; + +class ReactionRow extends ConsumerWidget { + final Message message; + const ReactionRow(this.message, {super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final clientState = ref.watch(ClientStateController.provider); + + return Wrap( + spacing: 4, + runSpacing: 4, + children: clientState?.homeserverUrl == null || message.reactions == null + ? [] + : message.reactions!.mapTo((reaction, reactors) { + final selected = reactors.contains(clientState!.userId); + return SizedBox( + child: Tooltip( + message: reactors.join(", "), + child: ChoiceChip( + showCheckmark: false, + selected: selected, + label: Row( + mainAxisSize: MainAxisSize.min, + spacing: 8, + children: [ + reaction.startsWith("mxc://") + ? Image( + height: 20, + image: CachedNetworkImage( + headers: ref.headers, + Uri.parse(reaction) + .mxcToHttps(clientState.homeserverUrl!) + .toString(), + ref.watch(CrossCacheController.provider), + ), + ) + : Text(reaction), + Text(reactors.length.toString()), + ], + ), + onSelected: (value) async { + final roomId = ref.watch( + SelectedRoomController.provider.select( + (value) => value?.metadata?.id, + ), + ); + if (roomId == null) return; + + final controller = ref.watch( + RoomChatController.provider(roomId).notifier, + ); + + if (selected) { + // TODO: remove + } else { + await controller.sendReaction(reaction, message); + } + }, + ), + ), + ); + }).toList(), + ); + } +}