diff --git a/packages/stream_chat_flutter/lib/src/message_widget/reactions/desktop_reactions_builder.dart b/packages/stream_chat_flutter/lib/src/message_widget/reactions/desktop_reactions_builder.dart index b681fff5..cbc55bdb 100644 --- a/packages/stream_chat_flutter/lib/src/message_widget/reactions/desktop_reactions_builder.dart +++ b/packages/stream_chat_flutter/lib/src/message_widget/reactions/desktop_reactions_builder.dart @@ -2,6 +2,7 @@ import 'package:collection/collection.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_portal/flutter_portal.dart'; +import 'package:stream_chat_flutter/src/message_widget/reactions/reactions_card.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; // ignore_for_file: cascade_invocations @@ -75,6 +76,7 @@ class _DesktopReactionsBuilderState extends State { @override Widget build(BuildContext context) { final streamChat = StreamChat.of(context); + final currentUser = streamChat.currentUser!; final reactionIcons = StreamChatConfiguration.of(context).reactionIcons; final streamChatTheme = StreamChatTheme.of(context); @@ -83,13 +85,13 @@ class _DesktopReactionsBuilderState extends State { if (widget.shouldShowReactions) { widget.message.latestReactions?.forEach((element) { if (!reactionsMap.containsKey(element.type) || - element.user!.id == streamChat.currentUser?.id) { + element.user!.id == currentUser.id) { reactionsMap[element.type] = element; } }); reactionsList = reactionsMap.values.toList() - ..sort((a, b) => a.user!.id == streamChat.currentUser?.id ? 1 : -1); + ..sort((a, b) => a.user!.id == currentUser.id ? 1 : -1); } return PortalTarget( @@ -114,44 +116,10 @@ class _DesktopReactionsBuilderState extends State { maxWidth: 336, maxHeight: 342, ), - child: Card( - color: streamChatTheme.colorTheme.barsBg, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(16), - ), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Padding( - padding: const EdgeInsets.all(16), - child: Text( - '''${widget.message.latestReactions!.length} ${context.translations.messageReactionsLabel}''', - style: streamChatTheme.textTheme.headlineBold, - ), - ), - Flexible( - child: SingleChildScrollView( - padding: const EdgeInsets.all(16), - child: Wrap( - spacing: 16, - runSpacing: 16, - children: [ - ...widget.message.latestReactions!.map((reaction) { - final reactionIcon = reactionIcons.firstWhereOrNull( - (r) => r.type == reaction.type, - ); - return _StackedReaction( - reaction: reaction, - streamChatTheme: streamChatTheme, - reactionIcon: reactionIcon, - ); - }).toList(), - ], - ), - ), - ), - ], - ), + child: ReactionsCard( + currentUser: currentUser, + message: widget.message, + messageTheme: widget.messageTheme, ), ), ), @@ -164,7 +132,10 @@ class _DesktopReactionsBuilderState extends State { setState(() => _showReactionsPopup = !_showReactionsPopup); }, child: Padding( - padding: const EdgeInsets.symmetric(vertical: 2), + padding: EdgeInsets.symmetric( + vertical: 2, + horizontal: widget.reverse ? 0 : 4, + ), child: Wrap( spacing: 4, runSpacing: 4, @@ -175,6 +146,7 @@ class _DesktopReactionsBuilderState extends State { ); return _BottomReaction( + currentUser: currentUser, reaction: reaction, message: widget.message, borderSide: widget.borderSide, @@ -193,6 +165,7 @@ class _DesktopReactionsBuilderState extends State { class _BottomReaction extends StatelessWidget { const _BottomReaction({ + required this.currentUser, required this.reaction, required this.message, required this.borderSide, @@ -201,6 +174,7 @@ class _BottomReaction extends StatelessWidget { required this.streamChatTheme, }); + final User currentUser; final Reaction reaction; final Message message; final BorderSide? borderSide; @@ -210,7 +184,7 @@ class _BottomReaction extends StatelessWidget { @override Widget build(BuildContext context) { - final userId = StreamChat.of(context).currentUser?.id; + final userId = currentUser.id; final backgroundColor = messageTheme?.reactionsBackgroundColor; @@ -264,8 +238,7 @@ class _BottomReaction extends StatelessWidget { size: 14, color: reaction.user?.id == userId ? streamChatTheme.colorTheme.accentPrimary - : streamChatTheme.colorTheme.textHighEmphasis - .withOpacity(0.5), + : streamChatTheme.colorTheme.textLowEmphasis, ), ), const SizedBox(width: 4), @@ -290,87 +263,3 @@ class _BottomReaction extends StatelessWidget { properties.add(DiagnosticsProperty('message', message)); } } - -class _StackedReaction extends StatelessWidget { - const _StackedReaction({ - required this.reaction, - required this.streamChatTheme, - required this.reactionIcon, - }); - - final Reaction reaction; - final StreamChatThemeData streamChatTheme; - final StreamReactionIcon? reactionIcon; - - @override - Widget build(BuildContext context) { - final userId = StreamChat.of(context).currentUser?.id; - return SizedBox( - width: 80, - child: Column( - children: [ - Stack( - children: [ - StreamUserAvatar( - user: reaction.user!, - constraints: const BoxConstraints.tightFor( - height: 64, - width: 64, - ), - borderRadius: BorderRadius.circular(32), - ), - Positioned( - bottom: 0, - right: 0, - child: DecoratedBox( - decoration: BoxDecoration( - color: streamChatTheme.colorTheme.inputBg, - border: Border.all( - color: streamChatTheme.colorTheme.barsBg, - width: 2, - ), - shape: BoxShape.circle, - ), - child: Padding( - padding: const EdgeInsets.all(8), - child: reactionIcon?.builder( - context, - reaction.userId == userId, - 16, - ) ?? - Icon( - Icons.help_outline_rounded, - size: 16, - color: reaction.user?.id == userId - ? streamChatTheme.colorTheme.accentPrimary - : streamChatTheme.colorTheme.textHighEmphasis - .withOpacity(0.5), - ), - ), - ), - ), - ], - ), - Text( - userId == reaction.user!.name ? 'You' : reaction.user!.name, - textAlign: TextAlign.center, - ), - ], - ), - ); - } - - @override - void debugFillProperties(DiagnosticPropertiesBuilder properties) { - super.debugFillProperties(properties); - properties.add( - DiagnosticsProperty('reaction', reaction), - ); - properties.add( - DiagnosticsProperty( - 'reactionIcon', - reactionIcon, - ), - ); - } -} diff --git a/packages/stream_chat_flutter/lib/src/message_widget/reactions/message_reactions_modal.dart b/packages/stream_chat_flutter/lib/src/message_widget/reactions/message_reactions_modal.dart index 04c749cc..cd66d08d 100644 --- a/packages/stream_chat_flutter/lib/src/message_widget/reactions/message_reactions_modal.dart +++ b/packages/stream_chat_flutter/lib/src/message_widget/reactions/message_reactions_modal.dart @@ -1,8 +1,8 @@ import 'dart:ui'; import 'package:flutter/material.dart'; -import 'package:stream_chat_flutter/src/message_widget/reactions/reaction_bubble.dart'; import 'package:stream_chat_flutter/src/message_widget/reactions/reactions_align.dart'; +import 'package:stream_chat_flutter/src/message_widget/reactions/reactions_card.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; /// {@template streamMessageReactionsModal} @@ -83,9 +83,10 @@ class StreamMessageReactionsModal extends StatelessWidget { ), if (message.latestReactions?.isNotEmpty == true) ...[ const SizedBox(height: 8), - _buildReactionCard( - context, - user, + ReactionsCard( + currentUser: user!, + message: message, + messageTheme: messageTheme, ), ], ], @@ -126,110 +127,4 @@ class StreamMessageReactionsModal extends StatelessWidget { ), ); } - - Widget _buildReactionCard(BuildContext context, User? user) { - final chatThemeData = StreamChatTheme.of(context); - return Card( - color: chatThemeData.colorTheme.barsBg, - clipBehavior: Clip.hardEdge, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(16), - ), - margin: EdgeInsets.zero, - child: Padding( - padding: const EdgeInsets.all(16), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Text( - context.translations.messageReactionsLabel, - style: chatThemeData.textTheme.headlineBold, - ), - const SizedBox(height: 16), - Flexible( - child: SingleChildScrollView( - child: Wrap( - spacing: 16, - runSpacing: 16, - children: message.latestReactions! - .map((e) => _buildReaction( - e, - user!, - context, - )) - .toList(), - ), - ), - ), - ], - ), - ), - ); - } - - Widget _buildReaction( - Reaction reaction, - User currentUser, - BuildContext context, - ) { - final isCurrentUser = reaction.user?.id == currentUser.id; - final chatThemeData = StreamChatTheme.of(context); - final reverse = !isCurrentUser; - return ConstrainedBox( - constraints: BoxConstraints.loose( - const Size(64, 100), - ), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Stack( - clipBehavior: Clip.none, - children: [ - StreamUserAvatar( - onTap: onUserAvatarTap, - user: reaction.user!, - constraints: const BoxConstraints.tightFor( - height: 64, - width: 64, - ), - onlineIndicatorConstraints: const BoxConstraints.tightFor( - height: 12, - width: 12, - ), - borderRadius: BorderRadius.circular(32), - ), - Positioned( - bottom: 6, - left: !reverse ? -3 : null, - right: reverse ? -3 : null, - child: Align( - alignment: - reverse ? Alignment.centerRight : Alignment.centerLeft, - child: StreamReactionBubble( - reactions: [reaction], - reverse: !reverse, - flipTail: !reverse, - borderColor: - messageTheme.reactionsBorderColor ?? Colors.transparent, - backgroundColor: messageTheme.reactionsBackgroundColor ?? - Colors.transparent, - maskColor: chatThemeData.colorTheme.barsBg, - tailCirclesSpacing: 1, - ), - ), - ), - ], - ), - const SizedBox(height: 8), - Text( - reaction.user!.name.split(' ')[0], - style: chatThemeData.textTheme.footnoteBold, - textAlign: TextAlign.center, - overflow: TextOverflow.ellipsis, - maxLines: 1, - ), - ], - ), - ); - } } diff --git a/packages/stream_chat_flutter/lib/src/message_widget/reactions/reaction_bubble.dart b/packages/stream_chat_flutter/lib/src/message_widget/reactions/reaction_bubble.dart index c3c3c762..82e5b34d 100644 --- a/packages/stream_chat_flutter/lib/src/message_widget/reactions/reaction_bubble.dart +++ b/packages/stream_chat_flutter/lib/src/message_widget/reactions/reaction_bubble.dart @@ -124,24 +124,22 @@ class StreamReactionBubble extends StatelessWidget { final chatThemeData = StreamChatTheme.of(context); final userId = StreamChat.of(context).currentUser?.id; return Padding( - padding: const EdgeInsets.symmetric( - horizontal: 4, - ), + padding: const EdgeInsets.symmetric(horizontal: 4), child: reactionIcon != null ? ConstrainedBox( - constraints: BoxConstraints.tight(const Size.square(16)), + constraints: BoxConstraints.tight(const Size.square(14)), child: reactionIcon.builder( context, - !highlightOwnReactions || reaction.user?.id == userId, + highlightOwnReactions && reaction.user?.id == userId, 16, ), ) : Icon( Icons.help_outline_rounded, - size: 16, - color: (!highlightOwnReactions || reaction.user?.id == userId) + size: 14, + color: (highlightOwnReactions && reaction.user?.id == userId) ? chatThemeData.colorTheme.accentPrimary - : chatThemeData.colorTheme.textHighEmphasis.withOpacity(0.5), + : chatThemeData.colorTheme.textLowEmphasis, ), ); } diff --git a/packages/stream_chat_flutter/lib/src/message_widget/reactions/reactions_card.dart b/packages/stream_chat_flutter/lib/src/message_widget/reactions/reactions_card.dart new file mode 100644 index 00000000..727b44af --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/message_widget/reactions/reactions_card.dart @@ -0,0 +1,142 @@ +import 'package:flutter/material.dart'; +import 'package:stream_chat_flutter/src/avatars/user_avatar.dart'; +import 'package:stream_chat_flutter/src/message_widget/reactions/reaction_bubble.dart'; +import 'package:stream_chat_flutter/src/theme/message_theme.dart'; +import 'package:stream_chat_flutter/src/theme/stream_chat_theme.dart'; +import 'package:stream_chat_flutter/src/utils/extensions.dart'; +import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; + +/// {@template reactionsCard} +/// A card that displays the reactions to a message. +/// +/// Used in [StreamMessageReactionsModal] and [DesktopReactionsBuilder]. +/// {@endtemplate} +class ReactionsCard extends StatelessWidget { + /// {@macro reactionsCard} + const ReactionsCard({ + super.key, + required this.currentUser, + required this.message, + required this.messageTheme, + this.onUserAvatarTap, + }); + + /// Current logged in user. + final User currentUser; + + /// Message to display reactions of. + final Message message; + + /// [StreamMessageThemeData] to apply to [message]. + final StreamMessageThemeData messageTheme; + + /// {@macro onUserAvatarTap} + final void Function(User)? onUserAvatarTap; + + @override + Widget build(BuildContext context) { + final chatThemeData = StreamChatTheme.of(context); + return Card( + color: chatThemeData.colorTheme.barsBg, + clipBehavior: Clip.hardEdge, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), + margin: EdgeInsets.zero, + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + context.translations.messageReactionsLabel, + style: chatThemeData.textTheme.headlineBold, + ), + const SizedBox(height: 16), + Flexible( + child: SingleChildScrollView( + child: Wrap( + spacing: 16, + runSpacing: 16, + children: message.latestReactions! + .map((e) => _buildReaction( + e, + currentUser, + context, + )) + .toList(), + ), + ), + ), + ], + ), + ), + ); + } + + Widget _buildReaction( + Reaction reaction, + User currentUser, + BuildContext context, + ) { + final isCurrentUser = reaction.user?.id == currentUser.id; + final chatThemeData = StreamChatTheme.of(context); + final reverse = !isCurrentUser; + return ConstrainedBox( + constraints: BoxConstraints.loose( + const Size(64, 100), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Stack( + clipBehavior: Clip.none, + children: [ + StreamUserAvatar( + onTap: onUserAvatarTap, + user: reaction.user!, + constraints: const BoxConstraints.tightFor( + height: 64, + width: 64, + ), + onlineIndicatorConstraints: const BoxConstraints.tightFor( + height: 12, + width: 12, + ), + borderRadius: BorderRadius.circular(32), + ), + Positioned( + bottom: 6, + left: !reverse ? -3 : null, + right: reverse ? -3 : null, + child: Align( + alignment: + reverse ? Alignment.centerRight : Alignment.centerLeft, + child: StreamReactionBubble( + reactions: [reaction], + reverse: !reverse, + flipTail: !reverse, + borderColor: + messageTheme.reactionsBorderColor ?? Colors.transparent, + backgroundColor: messageTheme.reactionsBackgroundColor ?? + Colors.transparent, + maskColor: chatThemeData.colorTheme.barsBg, + tailCirclesSpacing: 1, + ), + ), + ), + ], + ), + const SizedBox(height: 8), + Text( + reaction.user!.name.split(' ')[0], + style: chatThemeData.textTheme.footnoteBold, + textAlign: TextAlign.center, + overflow: TextOverflow.ellipsis, + maxLines: 1, + ), + ], + ), + ); + } +} diff --git a/packages/stream_chat_flutter/lib/src/misc/reaction_icon.dart b/packages/stream_chat_flutter/lib/src/misc/reaction_icon.dart index ebccc153..c1f6c144 100644 --- a/packages/stream_chat_flutter/lib/src/misc/reaction_icon.dart +++ b/packages/stream_chat_flutter/lib/src/misc/reaction_icon.dart @@ -1,5 +1,14 @@ import 'package:flutter/material.dart'; +/// {@template reactionIconBuilder} +/// Signature for a function that builds a reaction icon. +/// {@endtemplate} +typedef ReactionIconBuilder = Widget Function( + BuildContext context, + bool isHighlighted, + double iconSize, +); + /// {@template streamReactionIcon} /// Reaction icon data /// {@endtemplate} @@ -13,10 +22,6 @@ class StreamReactionIcon { /// Type of reaction final String type; - /// Asset to display for reaction - final Widget Function( - BuildContext, - bool highlighted, - double size, - ) builder; + /// {@macro reactionIconBuilder} + final ReactionIconBuilder builder; } diff --git a/packages/stream_chat_flutter/lib/src/stream_chat_configuration.dart b/packages/stream_chat_flutter/lib/src/stream_chat_configuration.dart index 76e4120f..379c1c14 100644 --- a/packages/stream_chat_flutter/lib/src/stream_chat_configuration.dart +++ b/packages/stream_chat_flutter/lib/src/stream_chat_configuration.dart @@ -165,7 +165,7 @@ class StreamChatConfigurationData { return StreamSvgIcon.loveReaction( color: highlighted ? theme.colorTheme.accentPrimary - : theme.primaryIconTheme.color!.withOpacity(0.5), + : theme.primaryIconTheme.color, size: size, ); }, @@ -177,7 +177,7 @@ class StreamChatConfigurationData { return StreamSvgIcon.thumbsUpReaction( color: highlighted ? theme.colorTheme.accentPrimary - : theme.primaryIconTheme.color!.withOpacity(0.5), + : theme.primaryIconTheme.color, size: size, ); }, @@ -189,7 +189,7 @@ class StreamChatConfigurationData { return StreamSvgIcon.thumbsDownReaction( color: highlighted ? theme.colorTheme.accentPrimary - : theme.primaryIconTheme.color!.withOpacity(0.5), + : theme.primaryIconTheme.color, size: size, ); }, @@ -201,7 +201,7 @@ class StreamChatConfigurationData { return StreamSvgIcon.lolReaction( color: highlighted ? theme.colorTheme.accentPrimary - : theme.primaryIconTheme.color!.withOpacity(0.5), + : theme.primaryIconTheme.color, size: size, ); }, @@ -213,7 +213,7 @@ class StreamChatConfigurationData { return StreamSvgIcon.wutReaction( color: highlighted ? theme.colorTheme.accentPrimary - : theme.primaryIconTheme.color!.withOpacity(0.5), + : theme.primaryIconTheme.color, size: size, ); }, diff --git a/packages/stream_chat_flutter/lib/src/theme/stream_chat_theme.dart b/packages/stream_chat_flutter/lib/src/theme/stream_chat_theme.dart index 02ac6f4c..04efaea6 100644 --- a/packages/stream_chat_flutter/lib/src/theme/stream_chat_theme.dart +++ b/packages/stream_chat_flutter/lib/src/theme/stream_chat_theme.dart @@ -126,8 +126,7 @@ class StreamChatThemeData { StreamTextTheme textTheme, ) { final accentColor = colorTheme.accentPrimary; - final iconTheme = - IconThemeData(color: colorTheme.textHighEmphasis.withOpacity(0.5)); + final iconTheme = IconThemeData(color: colorTheme.textLowEmphasis); final channelHeaderTheme = StreamChannelHeaderThemeData( avatarTheme: StreamAvatarThemeData( borderRadius: BorderRadius.circular(20),