231 lines
		
	
	
		
			6.6 KiB
		
	
	
	
		
			Dart
		
	
	
	
			
		
		
	
	
			231 lines
		
	
	
		
			6.6 KiB
		
	
	
	
		
			Dart
		
	
	
	
| import 'package:flutter/material.dart';
 | |
| 
 | |
| import 'package:collection/collection.dart' show IterableExtension;
 | |
| import 'package:matrix/matrix.dart';
 | |
| 
 | |
| import 'package:fluffychat/config/app_config.dart';
 | |
| import 'package:fluffychat/config/themes.dart';
 | |
| import 'package:fluffychat/widgets/avatar.dart';
 | |
| import 'package:fluffychat/widgets/future_loading_dialog.dart';
 | |
| import 'package:fluffychat/widgets/matrix.dart';
 | |
| import 'package:fluffychat/widgets/mxc_image.dart';
 | |
| 
 | |
| class MessageReactions extends StatelessWidget {
 | |
|   final Event event;
 | |
|   final Timeline timeline;
 | |
| 
 | |
|   const MessageReactions(this.event, this.timeline, {super.key});
 | |
| 
 | |
|   @override
 | |
|   Widget build(BuildContext context) {
 | |
|     final allReactionEvents =
 | |
|         event.aggregatedEvents(timeline, RelationshipTypes.reaction);
 | |
|     final reactionMap = <String, _ReactionEntry>{};
 | |
|     final client = Matrix.of(context).client;
 | |
| 
 | |
|     for (final e in allReactionEvents) {
 | |
|       final key = e.content
 | |
|           .tryGetMap<String, dynamic>('m.relates_to')
 | |
|           ?.tryGet<String>('key');
 | |
|       if (key != null) {
 | |
|         if (!reactionMap.containsKey(key)) {
 | |
|           reactionMap[key] = _ReactionEntry(
 | |
|             key: key,
 | |
|             count: 0,
 | |
|             reacted: false,
 | |
|             reactors: [],
 | |
|           );
 | |
|         }
 | |
|         reactionMap[key]!.count++;
 | |
|         reactionMap[key]!.reactors!.add(e.senderFromMemoryOrFallback);
 | |
|         reactionMap[key]!.reacted |= e.senderId == e.room.client.userID;
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     final reactionList = reactionMap.values.toList();
 | |
|     reactionList.sort((a, b) => b.count - a.count > 0 ? 1 : -1);
 | |
|     final ownMessage = event.senderId == event.room.client.userID;
 | |
|     return Wrap(
 | |
|       spacing: 4.0,
 | |
|       runSpacing: 4.0,
 | |
|       alignment: ownMessage ? WrapAlignment.end : WrapAlignment.start,
 | |
|       children: [
 | |
|         ...reactionList.map(
 | |
|           (r) => _Reaction(
 | |
|             reactionKey: r.key,
 | |
|             count: r.count,
 | |
|             reacted: r.reacted,
 | |
|             onTap: () {
 | |
|               if (r.reacted) {
 | |
|                 final evt = allReactionEvents.firstWhereOrNull(
 | |
|                   (e) =>
 | |
|                       e.senderId == e.room.client.userID &&
 | |
|                       e.content.tryGetMap('m.relates_to')?['key'] == r.key,
 | |
|                 );
 | |
|                 if (evt != null) {
 | |
|                   showFutureLoadingDialog(
 | |
|                     context: context,
 | |
|                     future: () => evt.redactEvent(),
 | |
|                   );
 | |
|                 }
 | |
|               } else {
 | |
|                 event.room.sendReaction(event.eventId, r.key);
 | |
|               }
 | |
|             },
 | |
|             onLongPress: () async => await _AdaptableReactorsDialog(
 | |
|               client: client,
 | |
|               reactionEntry: r,
 | |
|             ).show(context),
 | |
|           ),
 | |
|         ),
 | |
|         if (allReactionEvents.any((e) => e.status.isSending))
 | |
|           const SizedBox(
 | |
|             width: 24,
 | |
|             height: 24,
 | |
|             child: Padding(
 | |
|               padding: EdgeInsets.all(4.0),
 | |
|               child: CircularProgressIndicator.adaptive(strokeWidth: 1),
 | |
|             ),
 | |
|           ),
 | |
|       ],
 | |
|     );
 | |
|   }
 | |
| }
 | |
| 
 | |
| class _Reaction extends StatelessWidget {
 | |
|   final String reactionKey;
 | |
|   final int count;
 | |
|   final bool? reacted;
 | |
|   final void Function()? onTap;
 | |
|   final void Function()? onLongPress;
 | |
| 
 | |
|   const _Reaction({
 | |
|     required this.reactionKey,
 | |
|     required this.count,
 | |
|     required this.reacted,
 | |
|     required this.onTap,
 | |
|     required this.onLongPress,
 | |
|   });
 | |
| 
 | |
|   @override
 | |
|   Widget build(BuildContext context) {
 | |
|     final theme = Theme.of(context);
 | |
|     final textColor =
 | |
|         theme.brightness == Brightness.dark ? Colors.white : Colors.black;
 | |
|     final color = reacted == true
 | |
|         ? theme.bubbleColor
 | |
|         : theme.colorScheme.surfaceContainerHigh;
 | |
|     Widget content;
 | |
|     if (reactionKey.startsWith('mxc://')) {
 | |
|       content = Row(
 | |
|         mainAxisSize: MainAxisSize.min,
 | |
|         children: <Widget>[
 | |
|           MxcImage(
 | |
|             uri: Uri.parse(reactionKey),
 | |
|             width: 20,
 | |
|             height: 20,
 | |
|             animated: false,
 | |
|             isThumbnail: false,
 | |
|           ),
 | |
|           if (count > 1) ...[
 | |
|             const SizedBox(width: 4),
 | |
|             Text(
 | |
|               count.toString(),
 | |
|               style: TextStyle(
 | |
|                 color: textColor,
 | |
|                 fontSize: DefaultTextStyle.of(context).style.fontSize,
 | |
|               ),
 | |
|             ),
 | |
|           ],
 | |
|         ],
 | |
|       );
 | |
|     } else {
 | |
|       var renderKey = Characters(reactionKey);
 | |
|       if (renderKey.length > 10) {
 | |
|         renderKey = renderKey.getRange(0, 9) + Characters('…');
 | |
|       }
 | |
|       content = Text(
 | |
|         renderKey.toString() + (count > 1 ? ' $count' : ''),
 | |
|         style: TextStyle(
 | |
|           color: reacted == true ? theme.onBubbleColor : textColor,
 | |
|           fontSize: DefaultTextStyle.of(context).style.fontSize,
 | |
|         ),
 | |
|       );
 | |
|     }
 | |
|     return InkWell(
 | |
|       onTap: () => onTap != null ? onTap!() : null,
 | |
|       onLongPress: () => onLongPress != null ? onLongPress!() : null,
 | |
|       borderRadius: BorderRadius.circular(AppConfig.borderRadius / 2),
 | |
|       child: Container(
 | |
|         decoration: BoxDecoration(
 | |
|           color: color,
 | |
|           borderRadius: BorderRadius.circular(AppConfig.borderRadius / 2),
 | |
|         ),
 | |
|         padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 2),
 | |
|         child: content,
 | |
|       ),
 | |
|     );
 | |
|   }
 | |
| }
 | |
| 
 | |
| class _ReactionEntry {
 | |
|   String key;
 | |
|   int count;
 | |
|   bool reacted;
 | |
|   List<User>? reactors;
 | |
| 
 | |
|   _ReactionEntry({
 | |
|     required this.key,
 | |
|     required this.count,
 | |
|     required this.reacted,
 | |
|     this.reactors,
 | |
|   });
 | |
| }
 | |
| 
 | |
| class _AdaptableReactorsDialog extends StatelessWidget {
 | |
|   final Client? client;
 | |
|   final _ReactionEntry? reactionEntry;
 | |
| 
 | |
|   const _AdaptableReactorsDialog({
 | |
|     this.client,
 | |
|     this.reactionEntry,
 | |
|   });
 | |
| 
 | |
|   Future<bool?> show(BuildContext context) => showAdaptiveDialog(
 | |
|         context: context,
 | |
|         builder: (context) => this,
 | |
|         barrierDismissible: true,
 | |
|         useRootNavigator: false,
 | |
|       );
 | |
| 
 | |
|   @override
 | |
|   Widget build(BuildContext context) {
 | |
|     final body = SingleChildScrollView(
 | |
|       child: Wrap(
 | |
|         spacing: 8.0,
 | |
|         runSpacing: 4.0,
 | |
|         alignment: WrapAlignment.center,
 | |
|         children: <Widget>[
 | |
|           for (final reactor in reactionEntry!.reactors!)
 | |
|             Chip(
 | |
|               avatar: Avatar(
 | |
|                 mxContent: reactor.avatarUrl,
 | |
|                 name: reactor.displayName,
 | |
|                 client: client,
 | |
|                 presenceUserId: reactor.stateKey,
 | |
|               ),
 | |
|               label: Text(reactor.displayName!),
 | |
|             ),
 | |
|         ],
 | |
|       ),
 | |
|     );
 | |
| 
 | |
|     final title = Center(child: Text(reactionEntry!.key));
 | |
| 
 | |
|     return AlertDialog.adaptive(
 | |
|       title: title,
 | |
|       content: body,
 | |
|     );
 | |
|   }
 | |
| }
 |