From 069f1a8ee4c7dcee3e6b6105f1e3bfe42f395b53 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Thu, 15 Oct 2020 10:37:22 +0200 Subject: [PATCH] fix reaction bubble ui --- lib/src/message_widget.dart | 286 ++++++++------------------------- lib/src/reaction_bubble.dart | 193 ++++++++++++++++++++++ lib/src/reaction_picker.dart | 6 +- lib/src/stream_channel.dart | 4 +- lib/src/stream_chat.dart | 16 ++ lib/src/stream_chat_theme.dart | 45 ++++-- pubspec.yaml | 2 +- 7 files changed, 313 insertions(+), 239 deletions(-) create mode 100644 lib/src/reaction_bubble.dart diff --git a/lib/src/message_widget.dart b/lib/src/message_widget.dart index 841a2b26..f4f141bf 100644 --- a/lib/src/message_widget.dart +++ b/lib/src/message_widget.dart @@ -2,12 +2,13 @@ import 'dart:math'; import 'dart:ui'; import 'package:flutter/cupertino.dart'; +import 'package:flutter/foundation.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/src/reaction_bubble.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'image_group.dart'; @@ -198,9 +199,6 @@ class MessageWidget extends StatefulWidget { } class _MessageWidgetState extends State { - final GlobalKey _reactionPickerKey = GlobalKey(); - double _reactionPadding = 0; - @override Widget build(BuildContext context) { var leftPadding = widget.showUserAvatar != DisplayWidget.gone @@ -243,11 +241,15 @@ class _MessageWidgetState extends State { child: Padding( padding: widget.showReactions ? EdgeInsets.only( - top: _reactionPadding, + top: widget.message.reactionCounts + ?.isNotEmpty == + true + ? 16 + : 0, ) : EdgeInsets.zero, child: PortalEntry( - portalAnchor: Alignment(0, 1), + portalAnchor: Alignment(-0.81, 0), childAnchor: Alignment.topRight, portal: _buildReactionIndicator(context), child: (widget.message.isDeleted && @@ -296,57 +298,6 @@ class _MessageWidgetState extends State { ); } - @override - void didUpdateWidget(MessageWidget oldWidget) { - super.didUpdateWidget(oldWidget); - _updateReactionPadding(); - } - - @override - void initState() { - super.initState(); - _updateReactionPadding(); - } - - void _updateReactionPadding() { - WidgetsBinding.instance.addPostFrameCallback((timeStamp) { - if (!mounted) { - return; - } - if (_reactionPickerKey.currentContext != null && - widget.message.reactionCounts != null && - widget.message.reactionCounts.values - .where((element) => element > 0) - .isNotEmpty) { - setState(() { - _reactionPadding = _reactionPickerKey.currentContext.size.height; - }); - } else { - setState(() { - _reactionPadding = 0; - }); - } - }); - } - - Widget _buildReactionsTail(BuildContext context) { - return AnimatedSwitcher( - duration: Duration(milliseconds: 300), - child: widget.message.reactionCounts?.isNotEmpty == true - ? Transform.translate( - offset: Offset(4, 0), - child: CustomPaint( - painter: ReactionBubblePainter( - widget.messageTheme.reactionsBackgroundColor, - widget.messageTheme.reactionsBorderColor, - widget.message.reactionCounts.length, - ), - ), - ) - : SizedBox(), - ); - } - Padding _buildBottomRow(double leftPadding) { return Padding( padding: EdgeInsets.only( @@ -427,53 +378,72 @@ class _MessageWidgetState extends State { } Widget _buildReactionIndicator(BuildContext context) { + final otherReactions = widget.message.latestReactions + ?.where( + (element) => element.user.id != StreamChat.of(context).user.id) + ?.toList() ?? + []; + + var rowChildren = [ + if (widget.message.ownReactions?.isNotEmpty == true) ...[ + Transform.translate( + offset: Offset( + widget.reverse ? -6 : 0, + 0, + ), + child: ReactionBubble( + key: ValueKey('${widget.message.id}.own'), + reverse: widget.reverse, + backgroundColor: widget.messageTheme.ownReactionsBackgroundColor, + borderColor: widget.messageTheme.ownReactionsBorderColor, + reactions: widget.message.ownReactions, + ), + ), + if (otherReactions.isEmpty) + Container( + width: 18, + ), + ], + if (otherReactions?.isNotEmpty == true) ...[ + if (widget.message.ownReactions?.isEmpty == true) + Container( + width: 18, + ), + Transform.translate( + offset: Offset( + widget.reverse ? 0 : -6, + 0, + ), + child: ReactionBubble( + key: ValueKey('${widget.message.id}.other'), + reactions: otherReactions, + reverse: widget.reverse, + flipTail: true, + backgroundColor: widget.messageTheme.otherReactionsBackgroundColor, + borderColor: widget.messageTheme.otherReactionsBorderColor, + ), + ), + ] + ]; + + if (widget.reverse) { + rowChildren = rowChildren.reversed.toList(); + } return AnimatedSwitcher( - key: _reactionPickerKey, duration: Duration(milliseconds: 300), child: (widget.showReactions && - widget.message.reactionCounts?.isNotEmpty == true && + (widget.message.reactionCounts?.isNotEmpty == true || + otherReactions.isNotEmpty) && !widget.message.isDeleted) ? Container( child: GestureDetector( onTap: () => onLongPress(context), child: FractionallySizedBox( - widthFactor: 0.3, - child: Padding( - padding: const EdgeInsets.only( - bottom: 4.0, - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.center, - mainAxisSize: MainAxisSize.min, - children: [ - Transform( - transform: Matrix4.rotationY(widget.reverse ? pi : 0), - alignment: Alignment.center, - child: Container( - padding: const EdgeInsets.all(8), - decoration: BoxDecoration( - 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: _buildReactions(context), - ), - ), - _buildReactionsTail(context), - ], - ), + widthFactor: 0.5, + child: Row( + mainAxisAlignment: MainAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: rowChildren, ), ), ), @@ -482,36 +452,6 @@ class _MessageWidgetState extends State { ); } - 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(), - ], - ); - } - void _showMessageModalBottomSheet(BuildContext context) { final channel = StreamChannel.of(context).channel; showDialog( @@ -825,97 +765,3 @@ class _MessageWidgetState extends State { ); } } - -class ReactionBubblePainter extends CustomPainter { - final Color color; - final Color borderColor; - final int reactionsCount; - - ReactionBubblePainter( - this.color, - this.borderColor, - this.reactionsCount, - ); - - @override - void paint(Canvas canvas, Size size) { - _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.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); - } - - @override - bool shouldRepaint(CustomPainter oldDelegate) { - return true; - } -} diff --git a/lib/src/reaction_bubble.dart b/lib/src/reaction_bubble.dart new file mode 100644 index 00000000..46c2b238 --- /dev/null +++ b/lib/src/reaction_bubble.dart @@ -0,0 +1,193 @@ +import 'dart:math'; + +import 'package:flutter/material.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +class ReactionBubble extends StatelessWidget { + const ReactionBubble({ + Key key, + @required this.reactions, + @required this.borderColor, + @required this.backgroundColor, + this.reverse = false, + this.flipTail = false, + }) : super(key: key); + + final List reactions; + final Color borderColor; + final Color backgroundColor; + final bool reverse; + final bool flipTail; + + @override + Widget build(BuildContext context) { + final reactionAssets = StreamChatTheme.of(context).reactionAssets; + return Transform( + transform: Matrix4.rotationY(reverse ? pi : 0), + alignment: Alignment.center, + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: [ + Container( + padding: const EdgeInsets.all(4), + decoration: BoxDecoration( + border: Border.all( + color: borderColor, + ), + color: backgroundColor, + borderRadius: BorderRadius.all(Radius.circular(14)), + ), + child: Wrap( + children: [ + ...reactions.map((reaction) { + final reactionAsset = reactionAssets.firstWhere( + (reactionAsset) => reactionAsset.type == reaction.type, + 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(), + ], + ), + ), + _buildReactionsTail(context), + ], + ), + ); + } + + Widget _buildReactionsTail(BuildContext context) { + final tail = Transform.translate( + offset: Offset(4, 0), + child: CustomPaint( + painter: ReactionBubblePainter( + backgroundColor, + borderColor, + reactions.length, + ), + ), + ); + + if (!flipTail) { + return tail; + } else { + return Transform( + transform: Matrix4.rotationY(pi), + alignment: Alignment.center, + child: tail, + ); + } + } +} + +class ReactionBubblePainter extends CustomPainter { + final Color color; + final Color borderColor; + final int reactionsCount; + + ReactionBubblePainter( + this.color, + this.borderColor, + this.reactionsCount, + ); + + @override + void paint(Canvas canvas, Size size) { + _drawOval(size, canvas); + + _drawOvalBorder(size, canvas); + + _drawArc(size, canvas); + + _drawBorder(size, canvas); + } + + void _drawOvalBorder(Size size, Canvas canvas) { + final paint = Paint() + ..color = borderColor + ..strokeWidth = 1 + ..style = PaintingStyle.stroke; + + final path = Path(); + path.addOval( + Rect.fromCircle( + center: Offset(6, 2), + 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(6, 2), + 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 startAngle = reactionsCount > 1 ? 1.08 : 1.16; + final sweepAngle = reactionsCount > 1 ? 1.18 : 1.1; + final path = Path(); + path.addArc( + Rect.fromCircle( + center: Offset(0, dy), + radius: 4, + ), + -pi * startAngle, + -pi / sweepAngle, + ); + 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 startAngle = reactionsCount > 1 ? 1.05 : 1.16; + final path = Path(); + path.addArc( + Rect.fromCircle( + center: Offset(0, dy), + radius: 4, + ), + -pi * startAngle, + -pi, + ); + canvas.drawPath(path, paint); + } + + @override + bool shouldRepaint(CustomPainter oldDelegate) { + return true; + } +} diff --git a/lib/src/reaction_picker.dart b/lib/src/reaction_picker.dart index 6d9d2a64..c07c32cb 100644 --- a/lib/src/reaction_picker.dart +++ b/lib/src/reaction_picker.dart @@ -25,7 +25,7 @@ class ReactionPicker extends StatelessWidget { Widget build(BuildContext context) { final reactionAssets = StreamChatTheme.of(context).reactionAssets; return Material( - color: messageTheme.reactionsBackgroundColor, + color: messageTheme.ownReactionsBackgroundColor, clipBehavior: Clip.hardEdge, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(24), @@ -35,7 +35,7 @@ class ReactionPicker extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.center, mainAxisSize: MainAxisSize.min, children: reactionAssets.map((reactionAsset) { - final ownReactionIndex = message.latestReactions?.indexWhere( + final ownReactionIndex = message.ownReactions?.indexWhere( (reaction) => reaction.type == reactionAsset.type) ?? -1; return IconButton( @@ -45,7 +45,7 @@ class ReactionPicker extends StatelessWidget { package: reactionAsset.package, color: ownReactionIndex != -1 ? StreamChatTheme.of(context).accentColor - : Colors.black, + : Theme.of(context).iconTheme.color, ), onPressed: () { if (ownReactionIndex != -1) { diff --git a/lib/src/stream_channel.dart b/lib/src/stream_channel.dart index 21ee99f3..ce19b261 100644 --- a/lib/src/stream_channel.dart +++ b/lib/src/stream_channel.dart @@ -81,7 +81,9 @@ class StreamChannelState extends State { } _queryMessageController.add(false); }).catchError((e, stack) { - _queryMessageController.addError(e, stack); + if (!_queryMessageController.isClosed) { + _queryMessageController.addError(e, stack); + } }); } diff --git a/lib/src/stream_chat.dart b/lib/src/stream_chat.dart index dfc6095b..b22c257c 100644 --- a/lib/src/stream_chat.dart +++ b/lib/src/stream_chat.dart @@ -129,6 +129,14 @@ class StreamChatState extends State with WidgetsBindingObserver { constraints: themeData?.ownMessageTheme?.avatarTheme?.constraints, borderRadius: themeData?.ownMessageTheme?.avatarTheme?.borderRadius, ), + otherReactionsBorderColor: + themeData?.ownMessageTheme?.otherReactionsBorderColor, + otherReactionsBackgroundColor: + themeData?.ownMessageTheme?.otherReactionsBackgroundColor, + ownReactionsBackgroundColor: + themeData?.ownMessageTheme?.ownReactionsBackgroundColor, + ownReactionsBorderColor: + themeData?.ownMessageTheme?.ownReactionsBorderColor, ), otherMessageTheme: defaultTheme.otherMessageTheme.copyWith( replies: themeData?.otherMessageTheme?.replies, @@ -142,6 +150,14 @@ class StreamChatState extends State with WidgetsBindingObserver { constraints: themeData?.otherMessageTheme?.avatarTheme?.constraints, borderRadius: themeData?.otherMessageTheme?.avatarTheme?.borderRadius, ), + otherReactionsBorderColor: + themeData?.otherMessageTheme?.otherReactionsBorderColor, + otherReactionsBackgroundColor: + themeData?.otherMessageTheme?.otherReactionsBackgroundColor, + ownReactionsBackgroundColor: + themeData?.otherMessageTheme?.ownReactionsBackgroundColor, + ownReactionsBorderColor: + themeData?.otherMessageTheme?.ownReactionsBorderColor, ), accentColor: themeData?.accentColor, secondaryColor: themeData?.secondaryColor, diff --git a/lib/src/stream_chat_theme.dart b/lib/src/stream_chat_theme.dart index b208c035..0f92a294 100644 --- a/lib/src/stream_chat_theme.dart +++ b/lib/src/stream_chat_theme.dart @@ -302,8 +302,12 @@ class StreamChatThemeData { fontSize: 12, ), messageBackgroundColor: isDark ? Color(0xff191919) : Color(0xffEAEAEA), - reactionsBackgroundColor: isDark ? Colors.black : Colors.white, - reactionsBorderColor: isDark ? Color(0xff191919) : Color(0xffEAEAEA), + ownReactionsBackgroundColor: + isDark ? Color(0xff191919) : Color(0xffEAEAEA), + ownReactionsBorderColor: isDark ? Colors.black : Colors.white, + otherReactionsBackgroundColor: isDark ? Colors.black : Colors.white, + otherReactionsBorderColor: + isDark ? Color(0xff191919) : Color(0xffEAEAEA), avatarTheme: AvatarTheme( borderRadius: BorderRadius.circular(20), constraints: BoxConstraints.tightFor( @@ -316,9 +320,12 @@ class StreamChatThemeData { ), ), otherMessageTheme: MessageTheme( - reactionsBackgroundColor: + ownReactionsBackgroundColor: + isDark ? Color(0xff191919) : Color(0xffEAEAEA), + ownReactionsBorderColor: isDark ? Colors.black : Colors.white, + otherReactionsBackgroundColor: isDark ? Colors.black : Colors.white, + otherReactionsBorderColor: isDark ? Color(0xff191919) : Color(0xffEAEAEA), - reactionsBorderColor: theme.scaffoldBackgroundColor, messageText: TextStyle( fontSize: 15, color: isDark ? Colors.white : Colors.black, @@ -455,8 +462,10 @@ class MessageTheme { final TextStyle createdAt; final TextStyle replies; final Color messageBackgroundColor; - final Color reactionsBackgroundColor; - final Color reactionsBorderColor; + final Color ownReactionsBackgroundColor; + final Color ownReactionsBorderColor; + final Color otherReactionsBackgroundColor; + final Color otherReactionsBorderColor; final AvatarTheme avatarTheme; const MessageTheme({ @@ -465,8 +474,10 @@ class MessageTheme { this.messageAuthor, this.messageLinks, this.messageBackgroundColor, - this.reactionsBackgroundColor, - this.reactionsBorderColor, + this.ownReactionsBackgroundColor, + this.ownReactionsBorderColor, + this.otherReactionsBackgroundColor, + this.otherReactionsBorderColor, this.avatarTheme, this.createdAt, }); @@ -478,10 +489,11 @@ class MessageTheme { TextStyle createdAt, TextStyle replies, Color messageBackgroundColor, - Color otherMessageBackgroundColor, AvatarTheme avatarTheme, - Color reactionsBackgroundColor, - Color reactionsBorderColor, + Color ownReactionsBackgroundColor, + Color ownReactionsBorderColor, + Color otherReactionsBackgroundColor, + Color otherReactionsBorderColor, }) => MessageTheme( messageText: messageText ?? this.messageText, @@ -492,9 +504,14 @@ class MessageTheme { messageBackgroundColor ?? this.messageBackgroundColor, avatarTheme: avatarTheme ?? this.avatarTheme, replies: replies ?? this.replies, - reactionsBackgroundColor: - reactionsBackgroundColor ?? this.reactionsBackgroundColor, - reactionsBorderColor: reactionsBorderColor ?? this.reactionsBorderColor, + ownReactionsBackgroundColor: + ownReactionsBackgroundColor ?? this.ownReactionsBackgroundColor, + ownReactionsBorderColor: + ownReactionsBorderColor ?? this.ownReactionsBorderColor, + otherReactionsBackgroundColor: + otherReactionsBackgroundColor ?? this.otherReactionsBackgroundColor, + otherReactionsBorderColor: + otherReactionsBorderColor ?? this.otherReactionsBorderColor, ); } diff --git a/pubspec.yaml b/pubspec.yaml index 4ac8ef33..ecbeabe7 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.7+1 + stream_chat: ^0.2.8 mime: ^0.9.6+3 visibility_detector: ^0.1.5 line_awesome_icons: ^1.0.4+2