diff --git a/assets/Icon_LOL_reaction.svg b/assets/Icon_LOL_reaction.svg new file mode 100644 index 00000000..301fe02c --- /dev/null +++ b/assets/Icon_LOL_reaction.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/Icon_love_reaction.svg b/assets/Icon_love_reaction.svg new file mode 100644 index 00000000..76b09fd3 --- /dev/null +++ b/assets/Icon_love_reaction.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/Icon_thumbs_down_reaction.svg b/assets/Icon_thumbs_down_reaction.svg new file mode 100644 index 00000000..0d55ad2a --- /dev/null +++ b/assets/Icon_thumbs_down_reaction.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/Icon_thumbs_up_reaction.svg b/assets/Icon_thumbs_up_reaction.svg new file mode 100644 index 00000000..ff4dcc72 --- /dev/null +++ b/assets/Icon_thumbs_up_reaction.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/Icon_wut_reaction.svg b/assets/Icon_wut_reaction.svg new file mode 100644 index 00000000..c150b264 --- /dev/null +++ b/assets/Icon_wut_reaction.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icon_edit.svg b/assets/icon_edit.svg index 6979c575..32f9cea7 100644 --- a/assets/icon_edit.svg +++ b/assets/icon_edit.svg @@ -1,3 +1,3 @@ - - + + diff --git a/lib/src/message_actions_bottom_sheet.dart b/lib/src/message_actions_bottom_sheet.dart deleted file mode 100644 index 1e81900f..00000000 --- a/lib/src/message_actions_bottom_sheet.dart +++ /dev/null @@ -1,209 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:stream_chat/stream_chat.dart'; -import 'package:stream_chat_flutter/src/reaction_picker.dart'; -import 'package:stream_chat_flutter/src/stream_channel.dart'; - -import '../stream_chat_flutter.dart'; -import 'message_input.dart'; -import 'stream_chat.dart'; - -class MessageActionsBottomSheet extends StatelessWidget { - final Widget Function(BuildContext, Message) editMessageInputBuilder; - final void Function(Message) onThreadTap; - final Message message; - final bool showReactions; - final bool showDeleteMessage; - final bool showEditMessage; - final bool showReply; - final Map reactionToEmoji = const { - 'love': '❤️️', - 'haha': '😂', - 'like': '👍', - 'sad': '😕', - 'angry': '😡', - 'wow': '😲', - }; - - const MessageActionsBottomSheet({ - Key key, - this.message, - this.showReactions, - this.showDeleteMessage, - this.showEditMessage, - this.onThreadTap, - this.showReply, - this.editMessageInputBuilder, - }) : super(key: key); - - @override - Widget build(BuildContext context) { - final channel = StreamChannel.of(context).channel; - return SafeArea( - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - if (showReactions && - (message.status == MessageSendingStatus.SENT || - message.status == null)) - ReactionPicker( - channel: channel, - reactionToEmoji: reactionToEmoji, - message: message, - ), - if (showDeleteMessage) _buildDeleteButton(context), - if (showEditMessage) _buildEditMessage(context), - if (showReply && - (message.status == MessageSendingStatus.SENT || - message.status == null) && - message.parentId == null) - _buildReplyButton(context), - ], - ), - ); - } - - FlatButton _buildDeleteButton(BuildContext context) { - return FlatButton( - child: Padding( - padding: const EdgeInsets.all(16.0), - child: Text( - 'Delete message', - style: - Theme.of(context).textTheme.headline5.copyWith(color: Colors.red), - ), - ), - onPressed: () { - Navigator.pop(context); - StreamChat.of(context).client.deleteMessage( - message, - StreamChannel.of(context).channel.cid, - ); - }, - ); - } - - FlatButton _buildEditMessage(BuildContext context) { - return FlatButton( - child: Padding( - padding: const EdgeInsets.all(16.0), - child: Text( - 'Edit message', - style: Theme.of(context).textTheme.headline5, - ), - ), - onPressed: () async { - Navigator.pop(context); - _showEditBottomSheet(context); - }, - ); - } - - void _showEditBottomSheet(BuildContext context) { - final channel = StreamChannel.of(context).channel; - showModalBottomSheet( - context: context, - elevation: 2, - clipBehavior: Clip.hardEdge, - isScrollControlled: true, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.only( - topLeft: Radius.circular(32), - topRight: Radius.circular(32), - ), - ), - builder: (context) { - return StreamChannel( - channel: channel, - child: Flex( - direction: Axis.vertical, - mainAxisAlignment: MainAxisAlignment.end, - mainAxisSize: MainAxisSize.min, - children: [ - Padding( - padding: const EdgeInsets.only( - top: 16.0, - left: 16.0, - right: 16.0, - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - 'Edit message', - style: Theme.of(context).textTheme.headline6, - ), - Container( - height: 30, - padding: const EdgeInsets.all(2.0), - child: AspectRatio( - aspectRatio: 1, - child: RawMaterialButton( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(4), - ), - elevation: 0, - highlightElevation: 0, - focusElevation: 0, - disabledElevation: 0, - hoverElevation: 0, - onPressed: () { - Navigator.of(context).pop(); - }, - fillColor: - Theme.of(context).brightness == Brightness.dark - ? Colors.white.withOpacity(.1) - : Colors.black.withOpacity(.1), - padding: EdgeInsets.all(4), - child: Icon( - Icons.close, - size: 15, - color: StreamChatTheme.of(context) - .primaryIconTheme - .color, - ), - ), - ), - ), - ], - ), - ), - Padding( - padding: EdgeInsets.only( - bottom: MediaQuery.of(context).viewInsets.bottom, - ), - child: editMessageInputBuilder != null - ? editMessageInputBuilder(context, message) - : MessageInput( - editMessage: message, - onMessageSent: (_) { - FocusScope.of(context).unfocus(); - Navigator.pop(context); - }, - ), - ), - ], - ), - ); - }, - ); - } - - FlatButton _buildReplyButton(BuildContext context) { - return FlatButton( - child: Padding( - padding: const EdgeInsets.all(16.0), - child: Text( - 'Start a thread', - style: Theme.of(context).textTheme.headline5, - ), - ), - onPressed: () { - Navigator.pop(context); - if (onThreadTap != null) { - onThreadTap(message); - } - }, - ); - } -} diff --git a/lib/src/message_actions_modal.dart b/lib/src/message_actions_modal.dart index 5a089786..683f250b 100644 --- a/lib/src/message_actions_modal.dart +++ b/lib/src/message_actions_modal.dart @@ -6,9 +6,10 @@ import 'package:stream_chat/stream_chat.dart'; import 'package:stream_chat_flutter/src/reaction_picker.dart'; import 'package:stream_chat_flutter/src/stream_channel.dart'; -import '../stream_chat_flutter.dart'; import 'message_input.dart'; +import 'message_widget.dart'; import 'stream_chat.dart'; +import 'stream_chat_theme.dart'; class MessageActionsModal extends StatelessWidget { final Widget Function(BuildContext, Message) editMessageInputBuilder; @@ -21,14 +22,6 @@ class MessageActionsModal extends StatelessWidget { final bool showReply; final bool reverse; final ShapeBorder messageShape; - final Map reactionToEmoji = const { - 'love': '❤️️', - 'haha': '😂', - 'like': '👍', - 'sad': '😕', - 'angry': '😡', - 'wow': '😲', - }; const MessageActionsModal({ Key key, @@ -73,10 +66,12 @@ class MessageActionsModal extends StatelessWidget { if (showReactions && (message.status == MessageSendingStatus.SENT || message.status == null)) - ReactionPicker( - channel: channel, - reactionToEmoji: reactionToEmoji, - message: message, + Center( + child: ReactionPicker( + channel: channel, + message: message, + messageTheme: messageTheme, + ), ), AbsorbPointer( child: MessageWidget( @@ -158,7 +153,7 @@ class MessageActionsModal extends StatelessWidget { alignment: Alignment.center, package: 'stream_chat_flutter', color: StreamChatTheme.of(context).primaryIconTheme.color, - width: 17, + width: 24, ), onTap: () async { Navigator.pop(context); diff --git a/lib/src/message_widget.dart b/lib/src/message_widget.dart index c8160c06..5fea9fa0 100644 --- a/lib/src/message_widget.dart +++ b/lib/src/message_widget.dart @@ -5,6 +5,7 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter_portal/flutter_portal.dart'; +import 'package:flutter_svg/flutter_svg.dart'; import 'package:jiffy/jiffy.dart'; import 'package:stream_chat_flutter/src/message_actions_modal.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; @@ -193,15 +194,6 @@ class MessageWidget extends StatefulWidget { } class _MessageWidgetState extends State { - final Map _reactionToEmoji = { - 'love': '❤️️', - 'haha': '😂', - 'like': '👍', - 'sad': '😕', - 'angry': '😡', - 'wow': '😲', - }; - final GlobalKey _reactionPickerKey = GlobalKey(); double _reactionPadding = 0; @@ -341,9 +333,9 @@ class _MessageWidgetState extends State { offset: Offset(4, 0), child: CustomPaint( painter: ReactionBubblePainter( - Theme.of(context).brightness == Brightness.dark - ? Colors.white - : Colors.black, + widget.messageTheme.reactionsBackgroundColor, + widget.messageTheme.reactionsBorderColor, + widget.message.reactionCounts.length, ), ), ) @@ -456,14 +448,23 @@ class _MessageWidgetState extends State { child: Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( - color: Theme.of(context).brightness == - Brightness.dark - ? Colors.white - : Colors.black, + border: widget.messageTheme + .reactionsBackgroundColor == + StreamChatTheme.of(context) + .backgroundColor + ? Border.all( + color: Theme.of(context).brightness == + Brightness.dark + ? Colors.white.withAlpha(24) + : Colors.black.withAlpha(24), + ) + : null, + color: + widget.messageTheme.reactionsBackgroundColor, borderRadius: BorderRadius.all(Radius.circular(14)), ), - child: _buildReactionsText(context), + child: _buildReactions(context), ), ), _buildReactionsTail(context), @@ -477,18 +478,33 @@ class _MessageWidgetState extends State { ); } - Text _buildReactionsText(BuildContext context) { - return Text( - widget.message.reactionCounts.keys.map((reactionType) { - return _reactionToEmoji[reactionType] ?? '?'; - }).join(' ') + - ' ${widget.message.reactionCounts.values.fold(0, (t, v) => v + t).toString()}', - style: TextStyle( - color: Theme.of(context).brightness == Brightness.dark - ? Colors.black - : Colors.white, - ), - textAlign: TextAlign.justify, + Widget _buildReactions(BuildContext context) { + final reactionAssets = StreamChatTheme.of(context).reactionAssets; + return Row( + mainAxisSize: MainAxisSize.min, + children: [ + ...widget.message.reactionCounts.keys.map((reactionType) { + final reactionAsset = reactionAssets.firstWhere( + (reactionAsset) => reactionAsset.type == reactionType, + orElse: () => null, + ); + if (reactionAsset == null) { + return Text( + '?', + style: TextStyle( + color: StreamChatTheme.of(context).accentColor, + ), + ); + } + + return SvgPicture.asset( + reactionAsset.svgAsset, + package: reactionAsset.package, + height: 16, + color: StreamChatTheme.of(context).accentColor, + ); + }).toList(), + ], ); } @@ -807,17 +823,89 @@ class _MessageWidgetState extends State { class ReactionBubblePainter extends CustomPainter { final Color color; + final Color borderColor; + final int reactionsCount; - ReactionBubblePainter(this.color); + ReactionBubblePainter( + this.color, + this.borderColor, + this.reactionsCount, + ); @override void paint(Canvas canvas, Size size) { - final paint = Paint()..color = color; + _drawArc(size, canvas); + + _drawBorder(size, canvas); + + _drawOval(size, canvas); + + _drawOvalBorder(size, canvas); + } + + void _drawOvalBorder(Size size, Canvas canvas) { + final paint = Paint() + ..color = borderColor + ..strokeWidth = 1 + ..style = PaintingStyle.stroke; + final path = Path(); - path.lineTo(-2, -6); - path.lineTo(0, 10); - path.lineTo(10, -6); - path.lineTo(-2, -6); + path.addOval( + Rect.fromCircle( + center: Offset(0, 3), + radius: 2, + ), + ); + canvas.drawPath(path, paint); + } + + void _drawOval(Size size, Canvas canvas) { + final paint = Paint() + ..color = color + ..strokeWidth = 1; + + final path = Path(); + path.addOval(Rect.fromCircle( + center: Offset(0, 3), + radius: 2, + )); + canvas.drawPath(path, paint); + } + + void _drawBorder(Size size, Canvas canvas) { + final paint = Paint() + ..color = borderColor + ..strokeWidth = 1 + ..style = PaintingStyle.stroke; + + final dy = reactionsCount > 1 ? -2.0 : -3.0; + final path = Path(); + path.addArc( + Rect.fromCircle( + center: Offset(-4, dy), + radius: 4, + ), + -pi * 1.15, + -pi / 1.4, + ); + canvas.drawPath(path, paint); + } + + void _drawArc(Size size, Canvas canvas) { + final paint = Paint() + ..color = color + ..strokeWidth = 1; + + final dy = reactionsCount > 1 ? -2.0 : -3.0; + final path = Path(); + path.addArc( + Rect.fromCircle( + center: Offset(-4, dy), + radius: 4, + ), + -pi, + -pi, + ); canvas.drawPath(path, paint); } diff --git a/lib/src/reaction_asset.dart b/lib/src/reaction_asset.dart new file mode 100644 index 00000000..d33bbf26 --- /dev/null +++ b/lib/src/reaction_asset.dart @@ -0,0 +1,11 @@ +class ReactionAsset { + final String type; + final String svgAsset; + final String package; + + ReactionAsset({ + this.type, + this.svgAsset, + this.package, + }); +} diff --git a/lib/src/reaction_picker.dart b/lib/src/reaction_picker.dart index 885d937a..af9d36da 100644 --- a/lib/src/reaction_picker.dart +++ b/lib/src/reaction_picker.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; import '../stream_chat_flutter.dart'; @@ -11,60 +12,54 @@ import '../stream_chat_flutter.dart'; class ReactionPicker extends StatelessWidget { const ReactionPicker({ Key key, - @required this.reactionToEmoji, @required this.message, @required this.channel, - this.size = 30, + @required this.messageTheme, }) : super(key: key); - final Map reactionToEmoji; final Message message; - final double size; + final MessageTheme messageTheme; final Channel channel; @override Widget build(BuildContext context) { + final reactionAssets = StreamChatTheme.of(context).reactionAssets; return Material( - color: Colors.black87, + color: messageTheme.reactionsBackgroundColor, + clipBehavior: Clip.hardEdge, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(24), + ), child: Row( crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.center, mainAxisSize: MainAxisSize.min, - children: reactionToEmoji.keys.map((reactionType) { - final ownReactionIndex = message.ownReactions - ?.indexWhere((reaction) => reaction.type == reactionType) ?? + children: reactionAssets.map((reactionAsset) { + final ownReactionIndex = message.ownReactions?.indexWhere( + (reaction) => reaction.type == reactionAsset.type) ?? -1; - return Column( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.start, - children: [ - IconButton( - iconSize: size, - icon: Text( - reactionToEmoji[reactionType], - style: TextStyle( - fontSize: size - 10, - ), - ), - onPressed: () { - if (ownReactionIndex != -1) { - removeReaction( - context, message.ownReactions[ownReactionIndex]); - } else { - sendReaction(context, reactionType); - } - }, - ), - ownReactionIndex != -1 - ? Padding( - padding: const EdgeInsets.only(bottom: 4.0), - child: Text( - message.ownReactions[ownReactionIndex].score.toString(), - style: TextStyle(color: Colors.white), - ), - ) - : SizedBox(), - ], + return IconButton( + iconSize: 24, + icon: SvgPicture.asset( + reactionAsset.svgAsset, + package: reactionAsset.package, + color: ownReactionIndex != -1 + ? StreamChatTheme.of(context).accentColor + : Colors.black, + ), + onPressed: () { + if (ownReactionIndex != -1) { + removeReaction( + context, + message.ownReactions[ownReactionIndex], + ); + } else { + sendReaction( + context, + reactionAsset.type, + ); + } + }, ); }).toList(), ), diff --git a/lib/src/stream_chat_theme.dart b/lib/src/stream_chat_theme.dart index 57aeec53..b208c035 100644 --- a/lib/src/stream_chat_theme.dart +++ b/lib/src/stream_chat_theme.dart @@ -4,6 +4,8 @@ import 'package:stream_chat_flutter/src/channel_header.dart'; import 'package:stream_chat_flutter/src/channel_preview.dart'; import 'package:stream_chat_flutter/src/message_input.dart'; +import 'reaction_asset.dart'; + /// Inherited widget providing the [StreamChatThemeData] to the widget tree class StreamChatTheme extends InheritedWidget { final StreamChatThemeData data; @@ -72,6 +74,9 @@ class StreamChatThemeData { /// Primary icon theme final IconThemeData primaryIconTheme; + /// Assets used for rendering reactions + final List reactionAssets; + /// Create a theme from scratch StreamChatThemeData({ this.primaryColor, @@ -85,6 +90,7 @@ class StreamChatThemeData { this.defaultChannelImage, this.defaultUserImage, this.primaryIconTheme, + this.reactionAssets, }); /// Create a theme from a Material [Theme] @@ -135,6 +141,7 @@ class StreamChatThemeData { Widget Function(BuildContext, Channel) defaultChannelImage, Widget Function(BuildContext, User) defaultUserImage, IconThemeData primaryIconTheme, + List reactionAssets, }) => StreamChatThemeData( primaryColor: primaryColor ?? this.primaryColor, @@ -203,6 +210,7 @@ class StreamChatThemeData { this.otherMessageTheme.avatarTheme, ) ?? this.otherMessageTheme, + reactionAssets: reactionAssets ?? this.reactionAssets, ); /// Get the default Stream Chat theme @@ -293,7 +301,9 @@ class StreamChatThemeData { fontWeight: FontWeight.bold, fontSize: 12, ), - messageBackgroundColor: isDark ? Color(0xff191919) : Color(0xffE6E6E6), + messageBackgroundColor: isDark ? Color(0xff191919) : Color(0xffEAEAEA), + reactionsBackgroundColor: isDark ? Colors.black : Colors.white, + reactionsBorderColor: isDark ? Color(0xff191919) : Color(0xffEAEAEA), avatarTheme: AvatarTheme( borderRadius: BorderRadius.circular(20), constraints: BoxConstraints.tightFor( @@ -306,6 +316,9 @@ class StreamChatThemeData { ), ), otherMessageTheme: MessageTheme( + reactionsBackgroundColor: + isDark ? Color(0xff191919) : Color(0xffEAEAEA), + reactionsBorderColor: theme.scaffoldBackgroundColor, messageText: TextStyle( fontSize: 15, color: isDark ? Colors.white : Colors.black, @@ -333,6 +346,33 @@ class StreamChatThemeData { ), ), ), + reactionAssets: [ + ReactionAsset( + type: 'love', + svgAsset: 'assets/Icon_love_reaction.svg', + package: 'stream_chat_flutter', + ), + ReactionAsset( + type: 'thumbs_up', + svgAsset: 'assets/Icon_thumbs_up_reaction.svg', + package: 'stream_chat_flutter', + ), + ReactionAsset( + type: 'thumbs_down', + svgAsset: 'assets/Icon_thumbs_down_reaction.svg', + package: 'stream_chat_flutter', + ), + ReactionAsset( + type: 'lol', + svgAsset: 'assets/Icon_LOL_reaction.svg', + package: 'stream_chat_flutter', + ), + ReactionAsset( + type: 'wut', + svgAsset: 'assets/Icon_wut_reaction.svg', + package: 'stream_chat_flutter', + ), + ], ); } } @@ -415,6 +455,8 @@ class MessageTheme { final TextStyle createdAt; final TextStyle replies; final Color messageBackgroundColor; + final Color reactionsBackgroundColor; + final Color reactionsBorderColor; final AvatarTheme avatarTheme; const MessageTheme({ @@ -423,6 +465,8 @@ class MessageTheme { this.messageAuthor, this.messageLinks, this.messageBackgroundColor, + this.reactionsBackgroundColor, + this.reactionsBorderColor, this.avatarTheme, this.createdAt, }); @@ -436,6 +480,8 @@ class MessageTheme { Color messageBackgroundColor, Color otherMessageBackgroundColor, AvatarTheme avatarTheme, + Color reactionsBackgroundColor, + Color reactionsBorderColor, }) => MessageTheme( messageText: messageText ?? this.messageText, @@ -446,6 +492,9 @@ class MessageTheme { messageBackgroundColor ?? this.messageBackgroundColor, avatarTheme: avatarTheme ?? this.avatarTheme, replies: replies ?? this.replies, + reactionsBackgroundColor: + reactionsBackgroundColor ?? this.reactionsBackgroundColor, + reactionsBorderColor: reactionsBorderColor ?? this.reactionsBorderColor, ); } diff --git a/pubspec.yaml b/pubspec.yaml index c3dfbe4c..7012f360 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -24,7 +24,7 @@ dependencies: image_picker: ^0.6.7+2 flutter_keyboard_visibility: ^3.2.1 flutter_svg: ^0.18.0 - stream_chat: ^0.2.3 + stream_chat: ^0.2.3+3 mime: ^0.9.6+3 visibility_detector: ^0.1.5 line_awesome_icons: ^1.0.4+2