diff --git a/lib/controllers/message_controller.dart b/lib/controllers/message_controller.dart index f278d91..24356c2 100644 --- a/lib/controllers/message_controller.dart +++ b/lib/controllers/message_controller.dart @@ -2,9 +2,11 @@ import "package:collection/collection.dart"; import "package:fast_immutable_collections/fast_immutable_collections.dart"; import "package:flutter_chat_core/flutter_chat_core.dart"; import "package:flutter_riverpod/flutter_riverpod.dart"; +import "package:nexus/controllers/client_controller.dart"; import "package:nexus/controllers/client_state_controller.dart"; import "package:nexus/helpers/extensions/mxc_to_https.dart"; import "package:nexus/models/configs/message_config.dart"; +import "package:nexus/models/requests/get_related_events_request.dart"; class MessageController extends AsyncNotifier { final MessageConfig config; @@ -13,7 +15,8 @@ class MessageController extends AsyncNotifier { @override Future build() async { try { - if (config.event.relationType == "m.replace" && !config.includeEdits) { + if ((config.event.relationType == "m.replace" && !config.includeEdits) || + config.room.metadata == null) { return null; } @@ -65,10 +68,35 @@ class MessageController extends AsyncNotifier { final replyId = config.event.content["m.relates_to"]?["m.in_reply_to"]?["event_id"]; + final reactionEvents = await ref + .watch(ClientController.provider.notifier) + .getRelatedEvents( + GetRelatedEventsRequest( + roomId: config.room.metadata!.id, + eventId: config.event.eventId, + relationType: "m.annotation", + ), + ); + + final reactions = reactionEvents + ?.fold>>(IMap(), (acc, event) { + final key = event.content["m.relates_to"]?["key"]; + if (key == null) return acc; + + return acc.update( + key, + (list) => list.add(event.authorId), + ifAbsent: () => IList([event.authorId]), + ); + }) + .map((key, value) => MapEntry(key, value.unlock)) + .unlock; + final asText = Message.text( metadata: metadata, id: config.event.eventId, + reactions: reactions, authorId: event.authorId, text: content["formatted_body"] ?? content["body"] ?? "", replyToMessageId: replyId, @@ -80,6 +108,7 @@ class MessageController extends AsyncNotifier { Message toSystemMessage(String content) => Message.system( metadata: {...metadata, "body": content}, id: config.event.eventId, + reactions: reactions, authorId: event.authorId, deliveredAt: config.event.timestamp, text: content, @@ -104,6 +133,7 @@ class MessageController extends AsyncNotifier { null || "m.image" => Message.image( id: config.event.eventId, authorId: event.authorId, + reactions: reactions, source: source, replyToMessageId: replyId, metadata: metadata, @@ -116,6 +146,7 @@ class MessageController extends AsyncNotifier { size: content["info"]["size"], metadata: metadata, id: config.event.eventId, + reactions: reactions, authorId: event.authorId, source: source, replyToMessageId: replyId, @@ -159,6 +190,7 @@ class MessageController extends AsyncNotifier { // ignore: dead_code ? Message.unsupported( metadata: metadata, + reactions: reactions, id: config.event.eventId, authorId: event.authorId, replyToMessageId: replyId, diff --git a/lib/controllers/room_chat_controller.dart b/lib/controllers/room_chat_controller.dart index aef5226..d3da7c7 100644 --- a/lib/controllers/room_chat_controller.dart +++ b/lib/controllers/room_chat_controller.dart @@ -77,6 +77,7 @@ class RoomChatController extends AsyncNotifier { ref.onDispose( ref.listen(NewEventsController.provider(roomId), (_, next) async { for (final event in next) { + // TODO: Handle new reactions if (event.type == "m.room.redaction") { final controller = await future; final message = controller.messages.firstWhereOrNull( diff --git a/lib/widgets/chat_page/wrappers/message_wrapper.dart b/lib/widgets/chat_page/wrappers/message_wrapper.dart index 33de6db..79cb35f 100644 --- a/lib/widgets/chat_page/wrappers/message_wrapper.dart +++ b/lib/widgets/chat_page/wrappers/message_wrapper.dart @@ -1,19 +1,28 @@ +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:timeago/timeago.dart"; -class MessageWrapper extends StatelessWidget { +class MessageWrapper extends ConsumerWidget { final Message message; final Widget child; final MessageGroupStatus? groupStatus; const MessageWrapper(this.message, this.child, this.groupStatus, {super.key}); @override - Widget build(BuildContext context) { + Widget build(BuildContext context, WidgetRef ref) { final theme = Theme.of(context); final error = message.metadata?["error"]; + final clientState = ref.watch(ClientStateController.provider); + return ClipRRect( borderRadius: BorderRadius.all(Radius.circular(12)), child: AnimatedContainer( @@ -69,6 +78,49 @@ class MessageWrapper extends StatelessWidget { 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: 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(), + ), ], ), ),