diff --git a/example/ios/Flutter/.last_build_id b/example/ios/Flutter/.last_build_id index e32660276e..171fde316f 100644 --- a/example/ios/Flutter/.last_build_id +++ b/example/ios/Flutter/.last_build_id @@ -1 +1 @@ -06fe8d0d3d89937a6d33b1033e75ae96 \ No newline at end of file +0289cdadbd29bab804f275e3c5907d07 \ No newline at end of file diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index f289e1b7e2..23f0045b15 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -38,10 +38,10 @@ PODS: - Firebase/Messaging (6.26.0): - Firebase/CoreOnly - FirebaseMessaging (~> 4.4.1) - - firebase_core (0.5.0): + - firebase_core (0.5.0-1): - Firebase/CoreOnly (~> 6.26.0) - Flutter - - firebase_messaging (7.0.2): + - firebase_messaging (7.0.3): - Firebase/CoreOnly (~> 6.26.0) - Firebase/Messaging (~> 6.26.0) - firebase_core @@ -233,8 +233,8 @@ SPEC CHECKSUMS: DKPhotoGallery: fdfad5125a9fdda9cc57df834d49df790dbb4179 file_picker: 3e6c3790de664ccf9b882732d9db5eaf6b8d4eb1 Firebase: 7cf5f9c67f03cb3b606d1d6535286e1080e57eb6 - firebase_core: 3134fe79d257d430f163b558caf52a10a87efe8a - firebase_messaging: 2844c37f9ce87c0904b38fe435223161b1a71528 + firebase_core: 00e54a4744164a6b5a250b96dd1ad5afaba7a342 + firebase_messaging: 666d9994651b1ecf8c582b52dd913f3fa58c17ef FirebaseAnalyticsInterop: 3f86269c38ae41f47afeb43ebf32a001f58fcdae FirebaseCore: f42e5e5f382cdcf6b617ed737bf6c871a6947b17 FirebaseCoreDiagnostics: 770ac5958e1372ce67959ae4b4f31d8e127c3ac1 diff --git a/example/lib/main.dart b/example/lib/main.dart index ed47470162..89ed938169 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -75,7 +75,9 @@ void main() async { ); await client.setUser( - User(id: 'super-band-9'), + User(id: 'super-band-9', extraData: { + 'name': 'John Doe', + }), 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoic3VwZXItYmFuZC05In0.0L6lGoeLwkz0aZRUcpZKsvaXtNEDHBcezVTZ0oPq40A', ); diff --git a/example/pubspec.yaml b/example/pubspec.yaml index c3ab3b461b..a5a2f73f81 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -1,7 +1,7 @@ name: example description: A new Flutter project. -version: 1.0.28+30 +version: 1.0.30+32 environment: sdk: ">=2.2.2 <3.0.0" diff --git a/fonts/stream-icons.ttf b/fonts/stream-icons.ttf index 2e4d854086..80660da49b 100644 Binary files a/fonts/stream-icons.ttf and b/fonts/stream-icons.ttf differ diff --git a/lib/src/message_actions_modal.dart b/lib/src/message_actions_modal.dart index 31086885c5..890761d708 100644 --- a/lib/src/message_actions_modal.dart +++ b/lib/src/message_actions_modal.dart @@ -1,6 +1,8 @@ import 'dart:ui'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.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'; @@ -18,6 +20,7 @@ class MessageActionsModal extends StatelessWidget { final MessageTheme messageTheme; final bool showReactions; final bool showDeleteMessage; + final bool showCopyMessage; final bool showEditMessage; final bool showReply; final bool reverse; @@ -27,19 +30,19 @@ class MessageActionsModal extends StatelessWidget { Key key, @required this.message, @required this.messageTheme, - this.showReactions, - this.showDeleteMessage, - this.showEditMessage, + this.showReactions = true, + this.showDeleteMessage = true, + this.showEditMessage = true, this.onThreadTap, - this.showReply, + this.showCopyMessage = true, + this.showReply = true, this.editMessageInputBuilder, this.messageShape, - this.reverse, + this.reverse = false, }) : super(key: key); @override Widget build(BuildContext context) { - final channel = StreamChannel.of(context).channel; return Stack( children: [ Positioned.fill( @@ -68,13 +71,13 @@ class MessageActionsModal extends StatelessWidget { message.status == null)) Center( child: ReactionPicker( - channel: channel, message: message, messageTheme: messageTheme, ), ), AbsorbPointer( child: MessageWidget( + key: Key('MessageWidget'), reverse: reverse, message: message, messageTheme: messageTheme, @@ -103,13 +106,14 @@ class MessageActionsModal extends StatelessWidget { children: ListTile.divideTiles( context: context, tiles: [ - if (showEditMessage) _buildEditMessage(context), if (showReply && (message.status == MessageSendingStatus.SENT || message.status == null) && message.parentId == null) _buildReplyButton(context), + if (showEditMessage) _buildEditMessage(context), if (showDeleteMessage) _buildDeleteButton(context), + if (showCopyMessage) _buildCopyButton(context), ], ).toList(), ), @@ -142,6 +146,23 @@ class MessageActionsModal extends StatelessWidget { ); } + Widget _buildCopyButton(BuildContext context) { + return ListTile( + title: Text( + 'Copy message', + style: Theme.of(context).textTheme.headline6, + ), + leading: Icon( + StreamIcons.copy, + color: StreamChatTheme.of(context).primaryIconTheme.color, + ), + onTap: () async { + await Clipboard.setData(ClipboardData(text: message.text)); + Navigator.pop(context); + }, + ); + } + Widget _buildEditMessage(BuildContext context) { return ListTile( title: Text( @@ -256,7 +277,7 @@ class MessageActionsModal extends StatelessWidget { style: Theme.of(context).textTheme.headline6, ), leading: Icon( - StreamIcons.Thread_Reply, + StreamIcons.sorting_up, color: StreamChatTheme.of(context).primaryIconTheme.color, ), onTap: () { diff --git a/lib/src/message_reactions_modal.dart b/lib/src/message_reactions_modal.dart new file mode 100644 index 0000000000..d3486f1ba1 --- /dev/null +++ b/lib/src/message_reactions_modal.dart @@ -0,0 +1,199 @@ +import 'dart:ui'; + +import 'package:flutter/material.dart'; +import 'package:stream_chat/stream_chat.dart'; +import 'package:stream_chat_flutter/src/reaction_bubble.dart'; +import 'package:stream_chat_flutter/src/reaction_picker.dart'; +import 'package:stream_chat_flutter/src/stream_chat.dart'; +import 'package:stream_chat_flutter/src/user_avatar.dart'; + +import 'message_widget.dart'; +import 'stream_chat_theme.dart'; + +class MessageReactionsModal extends StatelessWidget { + final Widget Function(BuildContext, Message) editMessageInputBuilder; + final void Function(Message) onThreadTap; + final Message message; + final MessageTheme messageTheme; + final bool reverse; + final bool showReactions; + final ShapeBorder messageShape; + final void Function(User) onUserAvatarTap; + + const MessageReactionsModal({ + Key key, + @required this.message, + @required this.messageTheme, + this.showReactions = true, + this.onThreadTap, + this.editMessageInputBuilder, + this.messageShape, + this.reverse = false, + this.onUserAvatarTap, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Stack( + children: [ + Positioned.fill( + child: GestureDetector( + behavior: HitTestBehavior.translucent, + onTap: () { + Navigator.pop(context); + }, + child: BackdropFilter( + filter: ImageFilter.blur( + sigmaX: 10, + sigmaY: 10, + ), + child: Container( + color: Colors.transparent, + ), + ), + ), + ), + Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + if (showReactions && + (message.status == MessageSendingStatus.SENT || + message.status == null)) + Center( + child: ReactionPicker( + message: message, + messageTheme: messageTheme, + ), + ), + AbsorbPointer( + child: MessageWidget( + key: Key('MessageWidget'), + reverse: reverse, + message: message, + messageTheme: messageTheme, + showReactions: false, + showUsername: false, + showReplyIndicator: false, + showTimestamp: false, + showSendingIndicator: DisplayWidget.gone, + shape: messageShape, + ), + ), + SizedBox( + height: 16, + ), + if (message.latestReactions?.isNotEmpty == true) + Container( + constraints: BoxConstraints.loose(Size.fromHeight(400)), + child: _buildReactionCard(context), + ), + ], + ), + ], + ); + } + + Padding _buildReactionCard(BuildContext context) { + final currentUser = StreamChat.of(context).user; + return Padding( + padding: const EdgeInsets.symmetric( + horizontal: 8.0, + ), + child: Card( + clipBehavior: Clip.hardEdge, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: const EdgeInsets.all(16.0), + child: Text( + 'Message Reactions', + style: Theme.of(context).textTheme.headline6, + ), + ), + Flexible( + child: Padding( + padding: const EdgeInsets.only( + left: 16.0, + right: 16, + bottom: 16, + ), + child: GridView.builder( + shrinkWrap: true, + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 4, + crossAxisSpacing: 16, + childAspectRatio: 0.75, + mainAxisSpacing: 22, + ), + itemCount: message.latestReactions.length, + itemBuilder: (context, i) { + final reaction = message.latestReactions[i]; + + return _buildReaction( + reaction, + currentUser, + context, + ); + }, + ), + ), + ), + ], + ), + ), + ); + } + + Column _buildReaction( + Reaction reaction, + User currentUser, + BuildContext context, + ) { + final isCurrentUser = reaction.user.id == currentUser.id; + return Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Stack( + children: [ + UserAvatar( + onTap: onUserAvatarTap, + user: reaction.user, + constraints: BoxConstraints.tightFor( + height: 64, + width: 64, + ), + borderRadius: BorderRadius.circular(32), + ), + Positioned( + child: ReactionBubble( + reactions: [reaction], + borderColor: isCurrentUser + ? messageTheme.ownReactionsBorderColor + : messageTheme.otherReactionsBorderColor, + backgroundColor: isCurrentUser + ? messageTheme.ownReactionsBackgroundColor + : messageTheme.otherReactionsBackgroundColor, + flipTail: !isCurrentUser, + ), + bottom: 0, + left: isCurrentUser ? 0 : null, + right: isCurrentUser ? 0 : null, + ), + ], + ), + Text( + reaction.user.name, + style: Theme.of(context).textTheme.subtitle2, + textAlign: TextAlign.center, + ), + ], + ); + } +} diff --git a/lib/src/message_widget.dart b/lib/src/message_widget.dart index f4f141bfc0..967fb72b16 100644 --- a/lib/src/message_widget.dart +++ b/lib/src/message_widget.dart @@ -8,6 +8,7 @@ import 'package:flutter/rendering.dart'; import 'package:flutter_portal/flutter_portal.dart'; import 'package:jiffy/jiffy.dart'; import 'package:stream_chat_flutter/src/message_actions_modal.dart'; +import 'package:stream_chat_flutter/src/message_reactions_modal.dart'; import 'package:stream_chat_flutter/src/reaction_bubble.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; @@ -244,7 +245,7 @@ class _MessageWidgetState extends State { top: widget.message.reactionCounts ?.isNotEmpty == true - ? 16 + ? 12 : 0, ) : EdgeInsets.zero, @@ -437,7 +438,7 @@ class _MessageWidgetState extends State { !widget.message.isDeleted) ? Container( child: GestureDetector( - onTap: () => onLongPress(context), + onTap: () => _showMessageReactionsModalBottomSheet(context), child: FractionallySizedBox( widthFactor: 0.5, child: Row( @@ -452,7 +453,7 @@ class _MessageWidgetState extends State { ); } - void _showMessageModalBottomSheet(BuildContext context) { + void _showMessageActionModalBottomSheet(BuildContext context) { final channel = StreamChannel.of(context).channel; showDialog( context: context, @@ -476,6 +477,27 @@ class _MessageWidgetState extends State { }); } + void _showMessageReactionsModalBottomSheet(BuildContext context) { + final channel = StreamChannel.of(context).channel; + showDialog( + context: context, + builder: (context) { + return StreamChannel( + channel: channel, + child: MessageReactionsModal( + onUserAvatarTap: widget.onUserAvatarTap, + messageTheme: widget.messageTheme, + messageShape: widget.shape ?? _getDefaultShape(context), + reverse: widget.reverse, + message: widget.message, + editMessageInputBuilder: widget.editMessageInputBuilder, + onThreadTap: widget.onThreadTap, + showReactions: widget.showReactions, + ), + ); + }); + } + ContinuousRectangleBorder _getDefaultShape(BuildContext context) { return ContinuousRectangleBorder( side: widget.attachmentBorderSide ?? @@ -493,8 +515,9 @@ class _MessageWidgetState extends State { List _parseAttachments(BuildContext context) { final images = widget.message.attachments - .where((element) => element.type == 'image') - .toList(); + ?.where((element) => element.type == 'image') + ?.toList() ?? + []; if (images.length > 1) { return [ @@ -581,7 +604,7 @@ class _MessageWidgetState extends State { if (widget.onMessageActions != null) { widget.onMessageActions(context, widget.message); } else { - _showMessageModalBottomSheet(context); + _showMessageActionModalBottomSheet(context); } return; } diff --git a/lib/src/reaction_bubble.dart b/lib/src/reaction_bubble.dart index c9768c8d43..0d37d87f1a 100644 --- a/lib/src/reaction_bubble.dart +++ b/lib/src/reaction_bubble.dart @@ -21,12 +21,13 @@ class ReactionBubble extends StatelessWidget { @override Widget build(BuildContext context) { - final reactionAssets = StreamChatTheme.of(context).reactionIcons; + final reactionIcons = StreamChatTheme.of(context).reactionIcons; return Transform( transform: Matrix4.rotationY(reverse ? pi : 0), alignment: Alignment.center, child: Column( - crossAxisAlignment: CrossAxisAlignment.center, + crossAxisAlignment: + flipTail ? CrossAxisAlignment.start : CrossAxisAlignment.end, mainAxisSize: MainAxisSize.min, children: [ Container( @@ -41,11 +42,11 @@ class ReactionBubble extends StatelessWidget { child: Wrap( children: [ ...reactions.map((reaction) { - final reactionAsset = reactionAssets.firstWhere( - (reactionAsset) => reactionAsset.type == reaction.type, + final reactionIcon = reactionIcons.firstWhere( + (r) => r.type == reaction.type, orElse: () => null, ); - if (reactionAsset == null) { + if (reactionIcon == null) { return Text( '?', style: TextStyle( @@ -55,7 +56,7 @@ class ReactionBubble extends StatelessWidget { } return Icon( - reactionAsset.iconData, + reactionIcon.iconData, size: 16, color: StreamChatTheme.of(context).accentColor, ); @@ -71,7 +72,7 @@ class ReactionBubble extends StatelessWidget { Widget _buildReactionsTail(BuildContext context) { final tail = Transform.translate( - offset: Offset(4, 0), + offset: Offset(reactions.length > 1 ? -9 : -9, 0), child: CustomPaint( painter: ReactionBubblePainter( backgroundColor, @@ -152,7 +153,7 @@ class ReactionBubblePainter extends CustomPainter { 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 sweepAngle = reactionsCount > 1 ? 0.95 : 1.1; final path = Path(); path.addArc( Rect.fromCircle( @@ -171,7 +172,8 @@ class ReactionBubblePainter extends CustomPainter { ..strokeWidth = 1; final dy = reactionsCount > 1 ? -2.0 : -3.0; - final startAngle = reactionsCount > 1 ? 1.05 : 1.16; + final startAngle = reactionsCount > 1 ? 1 : 1.16; + final sweepAngle = reactionsCount > 1 ? 1.2 : 1; final path = Path(); path.addArc( Rect.fromCircle( @@ -179,7 +181,7 @@ class ReactionBubblePainter extends CustomPainter { radius: 4, ), -pi * startAngle, - -pi, + -pi * sweepAngle, ); canvas.drawPath(path, paint); } diff --git a/lib/src/reaction_picker.dart b/lib/src/reaction_picker.dart index 3b89023086..5855b59f50 100644 --- a/lib/src/reaction_picker.dart +++ b/lib/src/reaction_picker.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:stream_chat_flutter/src/reaction_bubble.dart'; import '../stream_chat_flutter.dart'; @@ -12,67 +13,81 @@ class ReactionPicker extends StatelessWidget { const ReactionPicker({ Key key, @required this.message, - @required this.channel, @required this.messageTheme, }) : super(key: key); final Message message; final MessageTheme messageTheme; - final Channel channel; @override Widget build(BuildContext context) { - final reactionAssets = StreamChatTheme.of(context).reactionIcons; - return Material( - color: messageTheme.ownReactionsBackgroundColor, - clipBehavior: Clip.hardEdge, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(24), - ), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.center, - mainAxisSize: MainAxisSize.min, - children: reactionAssets.map((reactionIcon) { - final ownReactionIndex = message.ownReactions?.indexWhere( - (reaction) => reaction.type == reactionIcon.type) ?? - -1; - return IconButton( - iconSize: 24, - icon: Icon( - reactionIcon.iconData, - color: ownReactionIndex != -1 - ? StreamChatTheme.of(context).accentColor - : Theme.of(context).iconTheme.color, + final reactionIcons = StreamChatTheme.of(context).reactionIcons; + return Stack( + fit: StackFit.passthrough, + children: [ + Material( + color: messageTheme.ownReactionsBackgroundColor, + clipBehavior: Clip.hardEdge, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(24), + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: reactionIcons.map((reactionIcon) { + final ownReactionIndex = message.ownReactions?.indexWhere( + (reaction) => reaction.type == reactionIcon.type) ?? + -1; + return IconButton( + iconSize: 24, + icon: Icon( + reactionIcon.iconData, + color: ownReactionIndex != -1 + ? StreamChatTheme.of(context).accentColor + : Theme.of(context).iconTheme.color, + ), + onPressed: () { + if (ownReactionIndex != -1) { + removeReaction( + context, + message.ownReactions[ownReactionIndex], + ); + } else { + sendReaction( + context, + reactionIcon.type, + ); + } + }, + ); + }).toList(), + ), + ), + Positioned( + right: 14, + bottom: 0, + child: CustomPaint( + painter: ReactionBubblePainter( + messageTheme.ownReactionsBackgroundColor, + messageTheme.ownReactionsBorderColor, + 2, ), - onPressed: () { - if (ownReactionIndex != -1) { - removeReaction( - context, - message.ownReactions[ownReactionIndex], - ); - } else { - sendReaction( - context, - reactionIcon.type, - ); - } - }, - ); - }).toList(), - ), + ), + ), + ], ); } /// Add a reaction to the message void sendReaction(BuildContext context, String reactionType) { - channel.sendReaction(message, reactionType); + StreamChannel.of(context).channel.sendReaction(message, reactionType); Navigator.of(context).pop(); } /// Remove a reaction from the message void removeReaction(BuildContext context, Reaction reaction) { - channel.deleteReaction(message, reaction); + StreamChannel.of(context).channel.deleteReaction(message, reaction); Navigator.of(context).pop(); } } diff --git a/lib/src/stream_chat_theme.dart b/lib/src/stream_chat_theme.dart index 56f71a937c..540cd7e198 100644 --- a/lib/src/stream_chat_theme.dart +++ b/lib/src/stream_chat_theme.dart @@ -142,7 +142,7 @@ class StreamChatThemeData { Widget Function(BuildContext, Channel) defaultChannelImage, Widget Function(BuildContext, User) defaultUserImage, IconThemeData primaryIconTheme, - List reactionAssets, + List reactionIcons, }) => StreamChatThemeData( primaryColor: primaryColor ?? this.primaryColor, @@ -211,7 +211,7 @@ class StreamChatThemeData { this.otherMessageTheme.avatarTheme, ) ?? this.otherMessageTheme, - reactionIcons: reactionAssets ?? this.reactionIcons, + reactionIcons: reactionIcons ?? this.reactionIcons, ); /// Get the default Stream Chat theme @@ -365,7 +365,7 @@ class StreamChatThemeData { ), ReactionIcon( type: 'thumbs_down', - iconData: StreamIcons.thumbs_up_reaction_1, + iconData: StreamIcons.thumbs_down_reaction, ), ReactionIcon( type: 'lol', diff --git a/lib/src/stream_icons.dart b/lib/src/stream_icons.dart index 73d1e5c041..c8fc649345 100644 --- a/lib/src/stream_icons.dart +++ b/lib/src/stream_icons.dart @@ -461,32 +461,32 @@ class StreamIcons { fontFamily: _fontFamily, fontPackage: _fontPackage, ); - static const IconData grey600 = IconData( + static const IconData grid = IconData( 0xe95b, fontFamily: _fontFamily, fontPackage: _fontPackage, ); - static const IconData grid = IconData( + static const IconData group = IconData( 0xe95c, fontFamily: _fontFamily, fontPackage: _fontPackage, ); - static const IconData heart_outline_add = IconData( + static const IconData heart_1 = IconData( 0xe95d, fontFamily: _fontFamily, fontPackage: _fontPackage, ); - static const IconData heart_outline_delete = IconData( + static const IconData heart_outline_add = IconData( 0xe95e, fontFamily: _fontFamily, fontPackage: _fontPackage, ); - static const IconData heart_pill = IconData( + static const IconData heart_outline_delete = IconData( 0xe95f, fontFamily: _fontFamily, fontPackage: _fontPackage, ); - static const IconData heart_1 = IconData( + static const IconData heart_pill = IconData( 0xe960, fontFamily: _fontFamily, fontPackage: _fontPackage, @@ -521,127 +521,127 @@ class StreamIcons { fontFamily: _fontFamily, fontPackage: _fontPackage, ); - static const IconData hourglass_full_1 = IconData( + static const IconData hourglass_full = IconData( 0xe967, fontFamily: _fontFamily, fontPackage: _fontPackage, ); - static const IconData hourglass_full = IconData( + static const IconData hourglass = IconData( 0xe968, fontFamily: _fontFamily, fontPackage: _fontPackage, ); - static const IconData hourglass = IconData( + static const IconData insurance_outline = IconData( 0xe969, fontFamily: _fontFamily, fontPackage: _fontPackage, ); - static const IconData insurance_outline = IconData( + static const IconData insurance = IconData( 0xe96a, fontFamily: _fontFamily, fontPackage: _fontPackage, ); - static const IconData insurance = IconData( + static const IconData left_right = IconData( 0xe96b, fontFamily: _fontFamily, fontPackage: _fontPackage, ); - static const IconData left_right = IconData( + static const IconData left = IconData( 0xe96c, fontFamily: _fontFamily, fontPackage: _fontPackage, ); - static const IconData left = IconData( + static const IconData light = IconData( 0xe96d, fontFamily: _fontFamily, fontPackage: _fontPackage, ); - static const IconData light = IconData( + static const IconData lightning = IconData( 0xe96e, fontFamily: _fontFamily, fontPackage: _fontPackage, ); - static const IconData lightning = IconData( + static const IconData link = IconData( 0xe96f, fontFamily: _fontFamily, fontPackage: _fontPackage, ); - static const IconData link = IconData( + static const IconData location_map = IconData( 0xe970, fontFamily: _fontFamily, fontPackage: _fontPackage, ); - static const IconData location_map = IconData( + static const IconData location = IconData( 0xe971, fontFamily: _fontFamily, fontPackage: _fontPackage, ); - static const IconData location = IconData( + static const IconData LOL_reaction = IconData( 0xe972, fontFamily: _fontFamily, fontPackage: _fontPackage, ); - static const IconData LOL_reaction = IconData( + static const IconData love_reaction = IconData( 0xe973, fontFamily: _fontFamily, fontPackage: _fontPackage, ); - static const IconData love_reaction = IconData( + static const IconData love = IconData( 0xe974, fontFamily: _fontFamily, fontPackage: _fontPackage, ); - static const IconData love = IconData( + static const IconData lungs = IconData( 0xe975, fontFamily: _fontFamily, fontPackage: _fontPackage, ); - static const IconData lungs = IconData( + static const IconData mail_open = IconData( 0xe976, fontFamily: _fontFamily, fontPackage: _fontPackage, ); - static const IconData mail_open = IconData( + static const IconData mail = IconData( 0xe977, fontFamily: _fontFamily, fontPackage: _fontPackage, ); - static const IconData mail = IconData( + static const IconData male = IconData( 0xe978, fontFamily: _fontFamily, fontPackage: _fontPackage, ); - static const IconData male = IconData( + static const IconData medical_bed = IconData( 0xe979, fontFamily: _fontFamily, fontPackage: _fontPackage, ); - static const IconData medical_bed = IconData( + static const IconData medical_blank = IconData( 0xe97a, fontFamily: _fontFamily, fontPackage: _fontPackage, ); - static const IconData medical_blank = IconData( + static const IconData medical_card_add = IconData( 0xe97b, fontFamily: _fontFamily, fontPackage: _fontPackage, ); - static const IconData medical_card_add = IconData( + static const IconData medical_card = IconData( 0xe97c, fontFamily: _fontFamily, fontPackage: _fontPackage, ); - static const IconData medical_card = IconData( + static const IconData medical_cross = IconData( 0xe97d, fontFamily: _fontFamily, fontPackage: _fontPackage, ); - static const IconData medical_cross = IconData( + static const IconData medical_load = IconData( 0xe97e, fontFamily: _fontFamily, fontPackage: _fontPackage, ); - static const IconData medical_load = IconData( + static const IconData menu_point_1 = IconData( 0xe97f, fontFamily: _fontFamily, fontPackage: _fontPackage, @@ -651,22 +651,22 @@ class StreamIcons { fontFamily: _fontFamily, fontPackage: _fontPackage, ); - static const IconData menu_point_1 = IconData( + static const IconData menu_point = IconData( 0xe981, fontFamily: _fontFamily, fontPackage: _fontPackage, ); - static const IconData menu_point = IconData( + static const IconData menu = IconData( 0xe982, fontFamily: _fontFamily, fontPackage: _fontPackage, ); - static const IconData menu = IconData( + static const IconData message = IconData( 0xe983, fontFamily: _fontFamily, fontPackage: _fontPackage, ); - static const IconData message = IconData( + static const IconData mic_1 = IconData( 0xe984, fontFamily: _fontFamily, fontPackage: _fontPackage, @@ -886,12 +886,12 @@ class StreamIcons { fontFamily: _fontFamily, fontPackage: _fontPackage, ); - static const IconData save_check = IconData( + static const IconData save_1 = IconData( 0xe9b0, fontFamily: _fontFamily, fontPackage: _fontPackage, ); - static const IconData save_1 = IconData( + static const IconData save_check = IconData( 0xe9b1, fontFamily: _fontFamily, fontPackage: _fontPackage, @@ -1061,17 +1061,17 @@ class StreamIcons { fontFamily: _fontFamily, fontPackage: _fontPackage, ); - static const IconData Thread_Reply = IconData( + static const IconData thumbs_down_reaction = IconData( 0xe9d3, fontFamily: _fontFamily, fontPackage: _fontPackage, ); - static const IconData thumbs_up_reaction_1 = IconData( + static const IconData thumbs_up_reaction = IconData( 0xe9d4, fontFamily: _fontFamily, fontPackage: _fontPackage, ); - static const IconData thumbs_up_reaction = IconData( + static const IconData time_1 = IconData( 0xe9d5, fontFamily: _fontFamily, fontPackage: _fontPackage, @@ -1081,108 +1081,103 @@ class StreamIcons { fontFamily: _fontFamily, fontPackage: _fontPackage, ); - static const IconData time_1 = IconData( - 0xe9d7, - fontFamily: _fontFamily, - fontPackage: _fontPackage, - ); static const IconData time = IconData( - 0xe9d8, + 0xe9d7, fontFamily: _fontFamily, fontPackage: _fontPackage, ); static const IconData tooth = IconData( - 0xe9d9, + 0xe9d8, fontFamily: _fontFamily, fontPackage: _fontPackage, ); static const IconData tube_full = IconData( - 0xe9da, + 0xe9d9, fontFamily: _fontFamily, fontPackage: _fontPackage, ); static const IconData tv_check = IconData( - 0xe9db, + 0xe9da, fontFamily: _fontFamily, fontPackage: _fontPackage, ); static const IconData tv_rating = IconData( - 0xe9dc, + 0xe9db, fontFamily: _fontFamily, fontPackage: _fontPackage, ); static const IconData unfold_less_h = IconData( - 0xe9dd, + 0xe9dc, fontFamily: _fontFamily, fontPackage: _fontPackage, ); static const IconData unfold_less = IconData( - 0xe9de, + 0xe9dd, fontFamily: _fontFamily, fontPackage: _fontPackage, ); static const IconData unfold_more_h = IconData( - 0xe9df, + 0xe9de, fontFamily: _fontFamily, fontPackage: _fontPackage, ); static const IconData unfold_more = IconData( - 0xe9e0, + 0xe9df, fontFamily: _fontFamily, fontPackage: _fontPackage, ); static const IconData unread = IconData( - 0xe9e1, + 0xe9e0, fontFamily: _fontFamily, fontPackage: _fontPackage, ); static const IconData up_down = IconData( - 0xe9e2, + 0xe9e1, fontFamily: _fontFamily, fontPackage: _fontPackage, ); static const IconData up = IconData( - 0xe9e3, + 0xe9e2, fontFamily: _fontFamily, fontPackage: _fontPackage, ); static const IconData upload = IconData( - 0xe9e4, + 0xe9e3, fontFamily: _fontFamily, fontPackage: _fontPackage, ); static const IconData user_add = IconData( - 0xe9e5, + 0xe9e4, fontFamily: _fontFamily, fontPackage: _fontPackage, ); static const IconData user_delete = IconData( - 0xe9e6, + 0xe9e5, fontFamily: _fontFamily, fontPackage: _fontPackage, ); static const IconData user_minus = IconData( - 0xe9e7, + 0xe9e6, fontFamily: _fontFamily, fontPackage: _fontPackage, ); static const IconData user = IconData( - 0xe9e8, + 0xe9e7, fontFamily: _fontFamily, fontPackage: _fontPackage, ); static const IconData voice = IconData( - 0xe9e9, + 0xe9e8, fontFamily: _fontFamily, fontPackage: _fontPackage, ); static const IconData world = IconData( - 0xe9ea, + 0xe9e9, fontFamily: _fontFamily, fontPackage: _fontPackage, ); static const IconData wut_reaction = IconData( - 0xe9eb, + 0xe9ea, fontFamily: _fontFamily, fontPackage: _fontPackage, ); diff --git a/lib/src/user_avatar.dart b/lib/src/user_avatar.dart index a3d5b6f578..a86206eb46 100644 --- a/lib/src/user_avatar.dart +++ b/lib/src/user_avatar.dart @@ -70,7 +70,7 @@ class UserAvatar extends StatelessWidget { : StreamChatTheme.of(context).defaultUserImage(context, user), ), ), - if (showOnlineStatus && user.online) + if (showOnlineStatus && user.online == true) Positioned( top: 0, right: 0, diff --git a/pubspec.yaml b/pubspec.yaml index 3fb1b5ffa4..2c1158aefe 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -29,6 +29,7 @@ dependencies: http_parser: ^3.1.4 flutter_slidable: ^0.5.4 carousel_slider: ^2.2.1 + clipboard: ^0.1.2+8 flutter: fonts: diff --git a/test/src/channel_preview_test.dart b/test/src/channel_preview_test.dart index 122e228332..94c45ac273 100644 --- a/test/src/channel_preview_test.dart +++ b/test/src/channel_preview_test.dart @@ -3,13 +3,7 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/mockito.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; -class MockClient extends Mock implements Client {} - -class MockClientState extends Mock implements ClientState {} - -class MockChannel extends Mock implements Channel {} - -class MockChannelState extends Mock implements ChannelClientState {} +import 'mocks.dart'; void main() { testWidgets( diff --git a/test/src/message_action_modal_test.dart b/test/src/message_action_modal_test.dart new file mode 100644 index 0000000000..1427edd69a --- /dev/null +++ b/test/src/message_action_modal_test.dart @@ -0,0 +1,94 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/mockito.dart'; +import 'package:stream_chat_flutter/src/message_actions_modal.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +import 'mocks.dart'; + +void main() { + testWidgets( + 'it should show the all actions', + (WidgetTester tester) async { + final client = MockClient(); + final clientState = MockClientState(); + + when(client.state).thenReturn(clientState); + when(clientState.user).thenReturn(OwnUser(id: 'user-id')); + + final themeData = ThemeData(); + final streamTheme = StreamChatThemeData.getDefaultTheme(themeData); + await tester.pumpWidget( + MaterialApp( + theme: themeData, + home: StreamChat( + streamChatThemeData: streamTheme, + client: client, + child: Container( + child: MessageActionsModal( + message: Message( + text: 'test', + user: User( + id: 'user-id', + ), + ), + messageTheme: streamTheme.ownMessageTheme, + ), + ), + ), + ), + ); + await tester.pump(); + + expect(find.byKey(Key('MessageWidget')), findsOneWidget); + expect(find.byIcon(StreamIcons.sorting_up), findsOneWidget); + expect(find.byIcon(StreamIcons.edit), findsOneWidget); + expect(find.byIcon(StreamIcons.delete), findsOneWidget); + expect(find.byIcon(StreamIcons.copy), findsOneWidget); + }, + ); + testWidgets( + 'it should show some actions', + (WidgetTester tester) async { + final client = MockClient(); + final clientState = MockClientState(); + + when(client.state).thenReturn(clientState); + when(clientState.user).thenReturn(OwnUser(id: 'user-id')); + + final themeData = ThemeData(); + final streamTheme = StreamChatThemeData.getDefaultTheme(themeData); + await tester.pumpWidget( + MaterialApp( + theme: themeData, + home: StreamChat( + streamChatThemeData: streamTheme, + client: client, + child: Container( + child: MessageActionsModal( + showEditMessage: false, + showCopyMessage: false, + showDeleteMessage: false, + showReply: false, + message: Message( + text: 'test', + user: User( + id: 'user-id', + ), + ), + messageTheme: streamTheme.ownMessageTheme, + ), + ), + ), + ), + ); + await tester.pump(); + + expect(find.byKey(Key('MessageWidget')), findsOneWidget); + expect(find.byIcon(StreamIcons.sorting_up), findsNothing); + expect(find.byIcon(StreamIcons.edit), findsNothing); + expect(find.byIcon(StreamIcons.delete), findsNothing); + expect(find.byIcon(StreamIcons.copy), findsNothing); + }, + ); +} diff --git a/test/src/message_reaction_modal_test.dart b/test/src/message_reaction_modal_test.dart new file mode 100644 index 0000000000..e397278030 --- /dev/null +++ b/test/src/message_reaction_modal_test.dart @@ -0,0 +1,88 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/mockito.dart'; +import 'package:stream_chat_flutter/src/message_reactions_modal.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +import 'mocks.dart'; + +void main() { + testWidgets( + 'it should show one thumbs from the picker', + (WidgetTester tester) async { + final themeData = ThemeData(); + final streamTheme = StreamChatThemeData.getDefaultTheme(themeData); + await tester.pumpWidget( + MaterialApp( + theme: themeData, + home: StreamChatTheme( + data: streamTheme, + child: MessageReactionsModal( + message: Message( + id: 'test', + text: 'test message', + user: User( + id: 'test-user', + ), + ), + messageTheme: streamTheme.ownMessageTheme, + ), + ), + ), + ); + + expect(find.byKey(Key('MessageWidget')), findsOneWidget); + expect(find.byIcon(StreamIcons.thumbs_up_reaction), findsOneWidget); + }, + ); + + testWidgets( + 'it should show two reactions', + (WidgetTester tester) async { + final client = MockClient(); + final clientState = MockClientState(); + + when(client.state).thenReturn(clientState); + when(clientState.user).thenReturn(OwnUser(id: 'user-id')); + + final themeData = ThemeData(); + final streamTheme = StreamChatThemeData.getDefaultTheme(themeData); + final testUserId = 'test user'; + + await tester.pumpWidget( + MaterialApp( + theme: themeData, + home: StreamChat( + streamChatThemeData: streamTheme, + client: client, + child: MessageReactionsModal( + message: Message( + text: 'test message', + user: User( + id: 'test-user', + ), + latestReactions: [ + Reaction( + type: 'thumbs_up', + user: User(id: testUserId), + ), + Reaction( + type: 'love', + user: User(id: testUserId), + ), + ], + ), + messageTheme: streamTheme.ownMessageTheme, + ), + ), + ), + ); + await tester.pump(); + + expect(find.byKey(Key('MessageWidget')), findsOneWidget); + expect(find.byIcon(StreamIcons.thumbs_up_reaction), findsNWidgets(2)); + expect(find.byIcon(StreamIcons.love_reaction), findsNWidgets(2)); + expect(find.text(testUserId), findsNWidgets(2)); + }, + ); +} diff --git a/test/src/mocks.dart b/test/src/mocks.dart new file mode 100644 index 0000000000..37fa5530b0 --- /dev/null +++ b/test/src/mocks.dart @@ -0,0 +1,10 @@ +import 'package:mockito/mockito.dart'; +import 'package:stream_chat/stream_chat.dart'; + +class MockClient extends Mock implements Client {} + +class MockClientState extends Mock implements ClientState {} + +class MockChannel extends Mock implements Channel {} + +class MockChannelState extends Mock implements ChannelClientState {} diff --git a/test/src/reaction_bubble_test.dart b/test/src/reaction_bubble_test.dart new file mode 100644 index 0000000000..dc0381dee6 --- /dev/null +++ b/test/src/reaction_bubble_test.dart @@ -0,0 +1,87 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:stream_chat_flutter/src/reaction_bubble.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +void main() { + testWidgets( + 'it should show no reactions', + (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + home: StreamChatTheme( + data: StreamChatThemeData(), + child: Container( + child: ReactionBubble( + reactions: [], + borderColor: Colors.black, + backgroundColor: Colors.white, + ), + ), + ), + ), + ); + + expect(find.byIcon(StreamIcons.thumbs_up_reaction), findsNothing); + }, + ); + + testWidgets( + 'it should show a thumb up', + (WidgetTester tester) async { + final themeData = ThemeData(); + await tester.pumpWidget( + MaterialApp( + theme: themeData, + home: StreamChatTheme( + data: StreamChatThemeData.getDefaultTheme(themeData), + child: Container( + child: ReactionBubble( + reactions: [ + Reaction( + type: 'thumbs_up', + ), + ], + borderColor: Colors.black, + backgroundColor: Colors.white, + ), + ), + ), + ), + ); + + expect(find.byIcon(StreamIcons.thumbs_up_reaction), findsOneWidget); + }, + ); + testWidgets( + 'it should show two reactions', + (WidgetTester tester) async { + final themeData = ThemeData(); + await tester.pumpWidget( + MaterialApp( + theme: themeData, + home: StreamChatTheme( + data: StreamChatThemeData.getDefaultTheme(themeData), + child: Container( + child: ReactionBubble( + reactions: [ + Reaction( + type: 'thumbs_up', + ), + Reaction( + type: 'love', + ), + ], + borderColor: Colors.black, + backgroundColor: Colors.white, + ), + ), + ), + ), + ); + + expect(find.byIcon(StreamIcons.thumbs_up_reaction), findsOneWidget); + expect(find.byIcon(StreamIcons.love_reaction), findsOneWidget); + }, + ); +}