add reaction picker

This commit is contained in:
Salvatore Giordano
2020-09-01 15:09:47 +02:00
parent 0997867944
commit 8c17b268ae
13 changed files with 247 additions and 301 deletions
-209
View File
@@ -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<String, String> 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: <Widget>[
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: <Widget>[
Padding(
padding: const EdgeInsets.only(
top: 16.0,
left: 16.0,
right: 16.0,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
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);
}
},
);
}
}
+9 -14
View File
@@ -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<String, String> 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);
+123 -35
View File
@@ -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<MessageWidget> {
final Map<String, String> _reactionToEmoji = {
'love': '❤️',
'haha': '😂',
'like': '👍',
'sad': '😕',
'angry': '😡',
'wow': '😲',
};
final GlobalKey _reactionPickerKey = GlobalKey();
double _reactionPadding = 0;
@@ -341,9 +333,9 @@ class _MessageWidgetState extends State<MessageWidget> {
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<MessageWidget> {
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<MessageWidget> {
);
}
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<MessageWidget> {
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);
}
+11
View File
@@ -0,0 +1,11 @@
class ReactionAsset {
final String type;
final String svgAsset;
final String package;
ReactionAsset({
this.type,
this.svgAsset,
this.package,
});
}
+34 -39
View File
@@ -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<String, String> 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: <Widget>[
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(),
),
+50 -1
View File
@@ -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<ReactionAsset> 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<ReactionAsset> 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,
);
}