import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:cached_network_image/cached_network_image.dart'; import 'package:future_loading_dialog/future_loading_dialog.dart'; import 'package:matrix/matrix.dart'; import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/utils/platform_infos.dart'; import 'package:fluffychat/widgets/avatar.dart'; import 'package:fluffychat/widgets/matrix.dart'; class MessageReactions extends StatelessWidget { final Event event; final Timeline timeline; const MessageReactions(this.event, this.timeline, {Key key}) : super(key: key); @override Widget build(BuildContext context) { final allReactionEvents = event.aggregatedEvents(timeline, RelationshipTypes.reaction); final reactionMap = {}; final client = Matrix.of(context).client; for (final e in allReactionEvents) { if (e.content['m.relates_to'].containsKey('key')) { final key = e.content['m.relates_to']['key']; if (!reactionMap.containsKey(key)) { reactionMap[key] = _ReactionEntry( key: key, count: 0, reacted: false, reactors: [], ); } reactionMap[key].count++; reactionMap[key].reactors.add(e.sender); 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); return Wrap(spacing: 4.0, runSpacing: 4.0, children: [ if (allReactionEvents.any((e) => e.status.isSending)) const SizedBox( width: 28, height: 28, child: Padding( padding: EdgeInsets.all(4.0), child: CircularProgressIndicator.adaptive(strokeWidth: 1), ), ), ...reactionList .map( (r) => _Reaction( reactionKey: r.key, count: r.count, reacted: r.reacted, onTap: () { if (r.reacted) { final evt = allReactionEvents.firstWhere( (e) => e.senderId == e.room.client.userID && e.content['m.relates_to']['key'] == r.key, orElse: () => null); if (evt != null) { showFutureLoadingDialog( context: context, future: () => evt.redactEvent(), ); } } else { showFutureLoadingDialog( context: context, future: () => event.room.sendReaction(event.eventId, r.key)); } }, onLongPress: () async => await _AdaptableReactorsDialog( client: client, reactionEntry: r, ).show(context), ), ) .toList(), ]); } } class _Reaction extends StatelessWidget { final String reactionKey; final int count; final bool reacted; final void Function() onTap; final void Function() onLongPress; const _Reaction({ this.reactionKey, this.count, this.reacted, this.onTap, this.onLongPress, }); @override Widget build(BuildContext context) { final textColor = Theme.of(context).brightness == Brightness.dark ? Colors.white : Colors.black; final color = Theme.of(context).scaffoldBackgroundColor; final fontSize = DefaultTextStyle.of(context).style.fontSize; Widget content; if (reactionKey.startsWith('mxc://')) { final src = Uri.parse(reactionKey)?.getThumbnail( Matrix.of(context).client, width: 9999, height: fontSize * MediaQuery.of(context).devicePixelRatio, method: ThumbnailMethod.scale, ); content = Row( mainAxisSize: MainAxisSize.min, children: [ CachedNetworkImage( imageUrl: src.toString(), height: fontSize, ), 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 $count', style: TextStyle( color: textColor, fontSize: DefaultTextStyle.of(context).style.fontSize, )); } return InkWell( onTap: () => onTap != null ? onTap() : null, onLongPress: () => onLongPress != null ? onLongPress() : null, borderRadius: BorderRadius.circular(AppConfig.borderRadius), child: Container( decoration: BoxDecoration( color: color, border: reacted ? Border.all( width: 1, color: Theme.of(context).primaryColor, ) : null, borderRadius: BorderRadius.circular(AppConfig.borderRadius), ), padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 3), child: content, ), ); } } class _ReactionEntry { String key; int count; bool reacted; List reactors; _ReactionEntry({this.key, this.count, this.reacted, this.reactors}); } class _AdaptableReactorsDialog extends StatelessWidget { final Client client; final _ReactionEntry reactionEntry; const _AdaptableReactorsDialog({ Key key, this.client, this.reactionEntry, }) : super(key: key); Future show(BuildContext context) => PlatformInfos.isCupertinoStyle ? showCupertinoDialog( context: context, builder: (context) => this, barrierDismissible: true, useRootNavigator: false, ) : showDialog( 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: [ for (var reactor in reactionEntry.reactors) Chip( avatar: Avatar( mxContent: reactor.avatarUrl, name: reactor.displayName, client: client, ), label: Text(reactor.displayName), ), ], ), ); final title = Center(child: Text(reactionEntry.key)); return PlatformInfos.isCupertinoStyle ? CupertinoAlertDialog( title: title, content: body, ) : AlertDialog( title: title, content: body, ); } }