Reaction support, currently readonly

This commit is contained in:
Henry Hiles 2026-04-08 15:47:53 -04:00
commit 5f5ad911c2
Signed by: Henry-Hiles
SSH key fingerprint: SHA256:VKQUdS31Q90KvX7EkKMHMBpUspcmItAh86a+v7PGiIs
3 changed files with 88 additions and 3 deletions

View file

@ -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<Message?> {
final MessageConfig config;
@ -13,7 +15,8 @@ class MessageController extends AsyncNotifier<Message?> {
@override
Future<Message?> 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<Message?> {
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<String, IList<String>>>(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?> {
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<Message?> {
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<Message?> {
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<Message?> {
// ignore: dead_code
? Message.unsupported(
metadata: metadata,
reactions: reactions,
id: config.event.eventId,
authorId: event.authorId,
replyToMessageId: replyId,

View file

@ -77,6 +77,7 @@ class RoomChatController extends AsyncNotifier<InMemoryChatController> {
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(

View file

@ -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(),
),
],
),
),