diff --git a/example/android/app/build.gradle b/example/android/app/build.gradle index dda667cf..474bfd75 100644 --- a/example/android/app/build.gradle +++ b/example/android/app/build.gradle @@ -31,15 +31,15 @@ android { sourceSets { main.java.srcDirs += 'src/main/kotlin' } - lintOptions { disable 'InvalidPackage' + checkReleaseBuilds false } defaultConfig { // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). applicationId "com.example.example" - minSdkVersion 16 + minSdkVersion 21 targetSdkVersion 28 versionCode flutterVersionCode.toInteger() versionName flutterVersionName diff --git a/example/android/build.gradle b/example/android/build.gradle index e1cfea50..8c57e18a 100644 --- a/example/android/build.gradle +++ b/example/android/build.gradle @@ -6,7 +6,7 @@ buildscript { } dependencies { - classpath 'com.android.tools.build:gradle:4.1.0' + classpath 'com.android.tools.build:gradle:3.6.2' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath 'com.google.gms:google-services:4.3.2' } diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index d22f1e0d..1e268608 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -80,6 +80,8 @@ PODS: - Flutter (1.0.0) - flutter_apns (0.0.1): - Flutter + - flutter_app_badger (0.0.1): + - Flutter - flutter_keyboard_visibility (0.0.1): - Flutter - flutter_local_notifications (0.0.1): @@ -160,6 +162,7 @@ DEPENDENCIES: - firebase_messaging (from `.symlinks/plugins/firebase_messaging/ios`) - Flutter (from `Flutter`) - flutter_apns (from `.symlinks/plugins/flutter_apns/ios`) + - flutter_app_badger (from `.symlinks/plugins/flutter_app_badger/ios`) - flutter_keyboard_visibility (from `.symlinks/plugins/flutter_keyboard_visibility/ios`) - flutter_local_notifications (from `.symlinks/plugins/flutter_local_notifications/ios`) - image_picker (from `.symlinks/plugins/image_picker/ios`) @@ -207,6 +210,8 @@ EXTERNAL SOURCES: :path: Flutter flutter_apns: :path: ".symlinks/plugins/flutter_apns/ios" + flutter_app_badger: + :path: ".symlinks/plugins/flutter_app_badger/ios" flutter_keyboard_visibility: :path: ".symlinks/plugins/flutter_keyboard_visibility/ios" flutter_local_notifications: @@ -244,6 +249,7 @@ SPEC CHECKSUMS: FirebaseMessaging: 29543feb343b09546ab3aa04d008ee8595b43c44 Flutter: 0e3d915762c693b495b44d77113d4970485de6ec flutter_apns: f516b118e423fe7c0a38771180549c4d6cb67c2f + flutter_app_badger: 65de4d6f0c34a891df49e6cfb8a1c0496426fa68 flutter_keyboard_visibility: 0339d06371254c3eb25eeb90ba8d17dca8f9c069 flutter_local_notifications: 0c0b1ae97e741e1521e4c1629a459d04b9aec743 FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a diff --git a/example/ios/Runner.xcodeproj/project.pbxproj b/example/ios/Runner.xcodeproj/project.pbxproj index db80aaed..6d377561 100644 --- a/example/ios/Runner.xcodeproj/project.pbxproj +++ b/example/ios/Runner.xcodeproj/project.pbxproj @@ -322,6 +322,7 @@ "${BUILT_PRODUCTS_DIR}/SwiftyGif/SwiftyGif.framework", "${BUILT_PRODUCTS_DIR}/file_picker/file_picker.framework", "${BUILT_PRODUCTS_DIR}/flutter_apns/flutter_apns.framework", + "${BUILT_PRODUCTS_DIR}/flutter_app_badger/flutter_app_badger.framework", "${BUILT_PRODUCTS_DIR}/flutter_keyboard_visibility/flutter_keyboard_visibility.framework", "${BUILT_PRODUCTS_DIR}/flutter_local_notifications/flutter_local_notifications.framework", "${BUILT_PRODUCTS_DIR}/image_picker/image_picker.framework", @@ -350,6 +351,7 @@ "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SwiftyGif.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/file_picker.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/flutter_apns.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/flutter_app_badger.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/flutter_keyboard_visibility.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/flutter_local_notifications.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/image_picker.framework", diff --git a/example/ios/Runner/Info.plist b/example/ios/Runner/Info.plist index ea713691..6c2c8e27 100644 --- a/example/ios/Runner/Info.plist +++ b/example/ios/Runner/Info.plist @@ -54,5 +54,9 @@ UIViewControllerBasedStatusBarAppearance + UIBackgroundModes + + remote-notification + diff --git a/example/lib/main.dart b/example/lib/main.dart index 89ed9381..667b1e0f 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -76,7 +76,7 @@ void main() async { await client.setUser( User(id: 'super-band-9', extraData: { - 'name': 'John Doe', + 'name': 'Jonathan Doe', }), 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoic3VwZXItYmFuZC05In0.0L6lGoeLwkz0aZRUcpZKsvaXtNEDHBcezVTZ0oPq40A', ); diff --git a/example/pubspec.yaml b/example/pubspec.yaml index cb0eca8e..c73804eb 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -1,6 +1,6 @@ name: example description: A new Flutter project. -version: 1.0.32+34 +version: 1.0.36+38 environment: sdk: ">=2.2.2 <3.0.0" diff --git a/lib/src/message_actions_modal.dart b/lib/src/message_actions_modal.dart index ce680d34..93e1c0db 100644 --- a/lib/src/message_actions_modal.dart +++ b/lib/src/message_actions_modal.dart @@ -43,88 +43,102 @@ class MessageActionsModal extends StatelessWidget { @override Widget build(BuildContext context) { - return Stack( - children: [ - Positioned.fill( - child: GestureDetector( - behavior: HitTestBehavior.translucent, - onTap: () { - Navigator.pop(context); - }, + final ownId = StreamChat.of(context).user.id; + return GestureDetector( + behavior: HitTestBehavior.translucent, + onTap: () { + Navigator.pop(context); + }, + child: Stack( + children: [ + Positioned.fill( child: BackdropFilter( filter: ImageFilter.blur( - sigmaX: 10, - sigmaY: 10, + sigmaX: 10.8731, + sigmaY: 10.8731, ), child: Container( - color: Colors.transparent, + color: Colors.black.withOpacity(0.2), ), ), ), - ), - 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.text.length > 200 - ? message.copyWith( - text: '${message.text.substring(0, 200)}...') - : message, - messageTheme: messageTheme, - showReactions: false, - showUsername: false, - showReplyIndicator: false, - showTimestamp: false, - showSendingIndicator: DisplayWidget.gone, - shape: messageShape, - ), - ), - SizedBox( - height: 8, - ), - Padding( - padding: const EdgeInsets.symmetric( - horizontal: 48.0, - ), - child: Material( - clipBehavior: Clip.hardEdge, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(16), - ), + Center( + child: SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0), child: Column( + mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.stretch, - children: ListTile.divideTiles( - context: context, - tiles: [ - 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(), + children: [ + if (showReactions && + (message.status == MessageSendingStatus.SENT || + message.status == null)) + Center( + child: ReactionPicker( + message: message, + messageTheme: messageTheme, + ), + ), + IgnorePointer( + child: MessageWidget( + key: Key('MessageWidget'), + reverse: reverse, + message: message.copyWith( + text: message.text.length > 200 + ? '${message.text.substring(0, 200)}...' + : message.text, + ), + messageTheme: messageTheme, + showReactions: false, + showUsername: false, + showReplyIndicator: false, + showUserAvatar: message.user.id == ownId + ? DisplayWidget.gone + : DisplayWidget.show, + showTimestamp: false, + showSendingIndicator: DisplayWidget.gone, + shape: messageShape, + ), + ), + SizedBox( + height: 8, + ), + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 48.0, + ), + child: Material( + clipBehavior: Clip.hardEdge, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: ListTile.divideTiles( + context: context, + tiles: [ + 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(), + ), + ), + ) + ], ), ), - ) - ], - ), - ], + ), + ), + ], + ), ); } diff --git a/lib/src/message_reactions_modal.dart b/lib/src/message_reactions_modal.dart index b30f92b3..375da94e 100644 --- a/lib/src/message_reactions_modal.dart +++ b/lib/src/message_reactions_modal.dart @@ -34,66 +34,75 @@ class MessageReactionsModal extends StatelessWidget { @override Widget build(BuildContext context) { - return Stack( - children: [ - Positioned.fill( - child: GestureDetector( - behavior: HitTestBehavior.translucent, - onTap: () { - Navigator.pop(context); - }, + final ownId = StreamChat.of(context).user.id; + return GestureDetector( + behavior: HitTestBehavior.translucent, + onTap: () { + Navigator.pop(context); + }, + child: Stack( + children: [ + Positioned.fill( child: BackdropFilter( filter: ImageFilter.blur( - sigmaX: 10, - sigmaY: 10, + sigmaX: 10.8731, + sigmaY: 10.8731, ), child: Container( - color: Colors.transparent, + color: Colors.black.withOpacity(0.2), ), ), ), - ), - Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - if (showReactions && - (message.status == MessageSendingStatus.SENT || - message.status == null)) - Center( - child: ReactionPicker( - message: message, - messageTheme: messageTheme, + Center( + child: SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + if (showReactions && + (message.status == MessageSendingStatus.SENT || + message.status == null)) + Center( + child: ReactionPicker( + message: message, + messageTheme: messageTheme, + ), + ), + IgnorePointer( + child: MessageWidget( + key: Key('MessageWidget'), + reverse: reverse, + message: message.copyWith( + text: message.text.length > 200 + ? '${message.text.substring(0, 200)}...' + : message.text, + ), + messageTheme: messageTheme, + showReactions: false, + showUsername: false, + showUserAvatar: message.user.id == ownId + ? DisplayWidget.gone + : DisplayWidget.show, + showReplyIndicator: false, + showTimestamp: false, + showSendingIndicator: DisplayWidget.gone, + shape: messageShape, + ), + ), + SizedBox( + height: 16, + ), + if (message.latestReactions?.isNotEmpty == true) + _buildReactionCard(context), + ], ), ), - AbsorbPointer( - child: MessageWidget( - key: Key('MessageWidget'), - reverse: reverse, - message: message.text.length > 200 - ? message.copyWith( - text: '${message.text.substring(0, 200)}...') - : 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), - ), - ], - ), - ], + ), + ], + ), ); } @@ -119,30 +128,25 @@ class MessageReactionsModal extends StatelessWidget { ), ), 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, + child: SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.only( + left: 18, + right: 18, + bottom: 26, + ), + child: Wrap( + spacing: 16, + runSpacing: 22, + alignment: WrapAlignment.start, + children: message.latestReactions + .map((e) => _buildReaction( + e, + currentUser, + context, + )) + .toList(), ), - itemCount: message.latestReactions.length, - itemBuilder: (context, i) { - final reaction = message.latestReactions[i]; - - return _buildReaction( - reaction, - currentUser, - context, - ); - }, ), ), ), @@ -152,51 +156,56 @@ class MessageReactionsModal extends StatelessWidget { ); } - Column _buildReaction( + Widget _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, + return ConstrainedBox( + constraints: BoxConstraints.loose(Size( + 64, + 98, + )), + child: 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), ), - borderRadius: BorderRadius.circular(32), - ), - Positioned( - child: ReactionBubble( - reactions: [reaction], - borderColor: isCurrentUser - ? messageTheme.ownReactionsBorderColor - : messageTheme.otherReactionsBorderColor, - backgroundColor: isCurrentUser - ? messageTheme.ownReactionsBackgroundColor - : messageTheme.otherReactionsBackgroundColor, - flipTail: !isCurrentUser, + Positioned( + child: Align( + alignment: Alignment.centerLeft, + child: ReactionBubble( + reactions: [reaction], + borderColor: messageTheme.reactionsBorderColor, + backgroundColor: messageTheme.reactionsBackgroundColor, + highlightOwnReactions: false, + ), + ), + bottom: 4, + left: isCurrentUser ? 0 : null, + right: isCurrentUser ? 0 : null, ), - bottom: 0, - left: isCurrentUser ? 0 : null, - right: isCurrentUser ? 0 : null, - ), - ], - ), - Text( - reaction.user.name, - style: Theme.of(context).textTheme.subtitle2, - textAlign: TextAlign.center, - ), - ], + ], + ), + Text( + reaction.user.name, + style: Theme.of(context).textTheme.subtitle2, + textAlign: TextAlign.center, + ), + ], + ), ); } } diff --git a/lib/src/message_text.dart b/lib/src/message_text.dart index 2ee6a6f8..ed636a57 100644 --- a/lib/src/message_text.dart +++ b/lib/src/message_text.dart @@ -24,7 +24,7 @@ class MessageText extends StatelessWidget { final text = _replaceMentions(message.text); return MarkdownBody( data: text, - onTapLink: (link) { + onTapLink: (text, link, title) { if (link.startsWith('@')) { final mentionedUser = message.mentionedUsers.firstWhere( (u) => '@${u.name.replaceAll(' ', '')}' == link, diff --git a/lib/src/message_widget.dart b/lib/src/message_widget.dart index 967fb72b..0e703fa0 100644 --- a/lib/src/message_widget.dart +++ b/lib/src/message_widget.dart @@ -239,20 +239,24 @@ class _MessageWidgetState extends State { 8, ), Flexible( - child: Padding( - padding: widget.showReactions - ? EdgeInsets.only( - top: widget.message.reactionCounts - ?.isNotEmpty == - true - ? 12 - : 0, - ) - : EdgeInsets.zero, - child: PortalEntry( - portalAnchor: Alignment(-0.81, 0), - childAnchor: Alignment.topRight, - portal: _buildReactionIndicator(context), + child: PortalEntry( + portal: Container( + transform: Matrix4.translationValues(-16, 2, 0), + child: _buildReactionIndicator(context), + constraints: BoxConstraints(maxWidth: 22 * 6.0), + ), + portalAnchor: Alignment(-1.0, -1.0), + childAnchor: Alignment(1, -1.0), + child: Padding( + padding: widget.showReactions + ? EdgeInsets.only( + top: widget.message.reactionCounts + ?.isNotEmpty == + true + ? 18 + : 0, + ) + : EdgeInsets.zero, child: (widget.message.isDeleted && widget.message.status != MessageSendingStatus.FAILED_DELETE) @@ -378,75 +382,33 @@ class _MessageWidgetState extends State { ); } - Widget _buildReactionIndicator(BuildContext context) { - final otherReactions = widget.message.latestReactions - ?.where( - (element) => element.user.id != StreamChat.of(context).user.id) - ?.toList() ?? - []; + Widget _buildReactionIndicator( + BuildContext context, + ) { + final ownId = StreamChat.of(context).user.id; + final reactionsMap = {}; + widget.message.latestReactions?.forEach((element) { + if (!reactionsMap.containsKey(element.type) || element.user.id == ownId) { + reactionsMap[element.type] = element; + } + }); + final reactionsList = reactionsMap.values.toList() + ..sort((a, b) => a.user.id == ownId ? 1 : -1); - 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( duration: Duration(milliseconds: 300), child: (widget.showReactions && - (widget.message.reactionCounts?.isNotEmpty == true || - otherReactions.isNotEmpty) && + (widget.message.reactionCounts?.isNotEmpty == true) && !widget.message.isDeleted) - ? Container( - child: GestureDetector( - onTap: () => _showMessageReactionsModalBottomSheet(context), - child: FractionallySizedBox( - widthFactor: 0.5, - child: Row( - mainAxisAlignment: MainAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: rowChildren, - ), - ), + ? GestureDetector( + onTap: () => _showMessageReactionsModalBottomSheet(context), + child: ReactionBubble( + key: ValueKey('${widget.message.id}.reactions'), + reverse: widget.reverse, + flipTail: widget.reverse, + backgroundColor: widget.messageTheme.reactionsBackgroundColor, + borderColor: widget.messageTheme.reactionsBorderColor, + reactions: reactionsList, ), ) : SizedBox(), @@ -498,8 +460,8 @@ class _MessageWidgetState extends State { }); } - ContinuousRectangleBorder _getDefaultShape(BuildContext context) { - return ContinuousRectangleBorder( + ShapeBorder _getDefaultShape(BuildContext context) { + return RoundedRectangleBorder( side: widget.attachmentBorderSide ?? widget.borderSide ?? BorderSide( @@ -718,7 +680,7 @@ class _MessageWidgetState extends State { onLongPress: () => onLongPress(context), child: Material( shape: widget.shape ?? - ContinuousRectangleBorder( + RoundedRectangleBorder( side: widget.borderSide ?? BorderSide( color: Theme.of(context).brightness == Brightness.dark diff --git a/lib/src/reaction_bubble.dart b/lib/src/reaction_bubble.dart index 0d37d87f..83593afc 100644 --- a/lib/src/reaction_bubble.dart +++ b/lib/src/reaction_bubble.dart @@ -1,6 +1,8 @@ import 'dart:math'; import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; +import 'package:stream_chat_flutter/src/reaction_icon.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; class ReactionBubble extends StatelessWidget { @@ -11,6 +13,7 @@ class ReactionBubble extends StatelessWidget { @required this.backgroundColor, this.reverse = false, this.flipTail = false, + this.highlightOwnReactions = true, }) : super(key: key); final List reactions; @@ -18,67 +21,100 @@ class ReactionBubble extends StatelessWidget { final Color backgroundColor; final bool reverse; final bool flipTail; + final bool highlightOwnReactions; @override Widget build(BuildContext context) { final reactionIcons = StreamChatTheme.of(context).reactionIcons; + final offset = reactions.length > 1 ? 16.0 : 2.0; return Transform( transform: Matrix4.rotationY(reverse ? pi : 0), alignment: Alignment.center, - child: Column( - crossAxisAlignment: - flipTail ? CrossAxisAlignment.start : CrossAxisAlignment.end, - mainAxisSize: MainAxisSize.min, + child: Stack( + alignment: Alignment.center, children: [ - Container( - padding: const EdgeInsets.all(4), - decoration: BoxDecoration( - border: Border.all( - color: borderColor, + Transform.translate( + offset: Offset(reverse ? offset : -offset, 0), + child: Container( + padding: const EdgeInsets.symmetric(vertical: 4), + decoration: BoxDecoration( + border: Border.all( + color: borderColor, + ), + color: backgroundColor, + borderRadius: BorderRadius.all(Radius.circular(14)), ), - color: backgroundColor, - borderRadius: BorderRadius.all(Radius.circular(14)), - ), - child: Wrap( - children: [ - ...reactions.map((reaction) { - final reactionIcon = reactionIcons.firstWhere( - (r) => r.type == reaction.type, - orElse: () => null, + child: LayoutBuilder( + builder: (context, constraints) { + return Flex( + direction: Axis.horizontal, + mainAxisSize: MainAxisSize.min, + children: [ + if (constraints.maxWidth < double.infinity) + ...reactions + .take((constraints.maxWidth) ~/ 22) + .map((reaction) { + return _buildReaction( + reactionIcons, + reaction, + context, + ); + }).toList(), + if (constraints.maxWidth == double.infinity) + ...reactions.map((reaction) { + return _buildReaction( + reactionIcons, + reaction, + context, + ); + }).toList(), + ], ); - if (reactionIcon == null) { - return Text( - '?', - style: TextStyle( - color: StreamChatTheme.of(context).accentColor, - ), - ); - } - - return Icon( - reactionIcon.iconData, - size: 16, - color: StreamChatTheme.of(context).accentColor, - ); - }).toList(), - ], + }, + ), ), ), - _buildReactionsTail(context), + Positioned( + bottom: 0, + left: reverse ? null : 11, + right: !reverse ? null : 11, + child: _buildReactionsTail(context), + ), ], ), ); } + Widget _buildReaction( + List reactionIcons, + Reaction reaction, + BuildContext context, + ) { + final reactionIcon = reactionIcons.firstWhere( + (r) => r.type == reaction.type, + orElse: () => null, + ); + + return Padding( + padding: const EdgeInsets.symmetric( + horizontal: 4.0, + ), + child: Icon( + reactionIcon?.iconData ?? Icons.help_outline_rounded, + size: 16, + color: (!highlightOwnReactions || + reaction.user.id == StreamChat.of(context).user.id) + ? StreamChatTheme.of(context).accentColor + : Colors.black.withOpacity(.5), + ), + ); + } + Widget _buildReactionsTail(BuildContext context) { - final tail = Transform.translate( - offset: Offset(reactions.length > 1 ? -9 : -9, 0), - child: CustomPaint( - painter: ReactionBubblePainter( - backgroundColor, - borderColor, - reactions.length, - ), + final tail = CustomPaint( + painter: ReactionBubblePainter( + backgroundColor, + borderColor, ), ); @@ -97,12 +133,10 @@ class ReactionBubble extends StatelessWidget { class ReactionBubblePainter extends CustomPainter { final Color color; final Color borderColor; - final int reactionsCount; ReactionBubblePainter( this.color, this.borderColor, - this.reactionsCount, ); @override @@ -125,7 +159,7 @@ class ReactionBubblePainter extends CustomPainter { final path = Path(); path.addOval( Rect.fromCircle( - center: Offset(6, 2), + center: Offset(4, 3), radius: 2, ), ); @@ -139,7 +173,7 @@ class ReactionBubblePainter extends CustomPainter { final path = Path(); path.addOval(Rect.fromCircle( - center: Offset(6, 2), + center: Offset(4, 3), radius: 2, )); canvas.drawPath(path, paint); @@ -151,13 +185,13 @@ class ReactionBubblePainter extends CustomPainter { ..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 ? 0.95 : 1.1; + final dy = -2.2; + final startAngle = 1.1; + final sweepAngle = 1.2; final path = Path(); path.addArc( Rect.fromCircle( - center: Offset(0, dy), + center: Offset(1, dy), radius: 4, ), -pi * startAngle, @@ -171,13 +205,13 @@ class ReactionBubblePainter extends CustomPainter { ..color = color ..strokeWidth = 1; - final dy = reactionsCount > 1 ? -2.0 : -3.0; - final startAngle = reactionsCount > 1 ? 1 : 1.16; - final sweepAngle = reactionsCount > 1 ? 1.2 : 1; + final dy = -2.2; + final startAngle = 1; + final sweepAngle = 1.3; final path = Path(); path.addArc( Rect.fromCircle( - center: Offset(0, dy), + center: Offset(1, dy), radius: 4, ), -pi * startAngle, diff --git a/lib/src/reaction_picker.dart b/lib/src/reaction_picker.dart index 5855b59f..87487cde 100644 --- a/lib/src/reaction_picker.dart +++ b/lib/src/reaction_picker.dart @@ -26,7 +26,7 @@ class ReactionPicker extends StatelessWidget { fit: StackFit.passthrough, children: [ Material( - color: messageTheme.ownReactionsBackgroundColor, + color: messageTheme.reactionsBackgroundColor, clipBehavior: Clip.hardEdge, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(24), @@ -45,7 +45,7 @@ class ReactionPicker extends StatelessWidget { reactionIcon.iconData, color: ownReactionIndex != -1 ? StreamChatTheme.of(context).accentColor - : Theme.of(context).iconTheme.color, + : Theme.of(context).iconTheme.color.withOpacity(.5), ), onPressed: () { if (ownReactionIndex != -1) { @@ -69,9 +69,8 @@ class ReactionPicker extends StatelessWidget { bottom: 0, child: CustomPaint( painter: ReactionBubblePainter( - messageTheme.ownReactionsBackgroundColor, - messageTheme.ownReactionsBorderColor, - 2, + messageTheme.reactionsBackgroundColor, + messageTheme.reactionsBorderColor, ), ), ), diff --git a/lib/src/stream_chat.dart b/lib/src/stream_chat.dart index b22c257c..e32c7fa1 100644 --- a/lib/src/stream_chat.dart +++ b/lib/src/stream_chat.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_app_badger/flutter_app_badger.dart'; import 'package:stream_chat/stream_chat.dart'; import 'package:stream_chat_flutter/src/stream_chat_theme.dart'; @@ -129,14 +130,9 @@ 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, + reactionsBorderColor: themeData?.ownMessageTheme?.reactionsBorderColor, + reactionsBackgroundColor: + themeData?.ownMessageTheme?.reactionsBackgroundColor, ), otherMessageTheme: defaultTheme.otherMessageTheme.copyWith( replies: themeData?.otherMessageTheme?.replies, @@ -150,14 +146,10 @@ 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, + reactionsBorderColor: + themeData?.otherMessageTheme?.reactionsBorderColor, + reactionsBackgroundColor: + themeData?.otherMessageTheme?.reactionsBackgroundColor, ), accentColor: themeData?.accentColor, secondaryColor: themeData?.secondaryColor, @@ -186,6 +178,13 @@ class StreamChatState extends State with WidgetsBindingObserver { void initState() { super.initState(); WidgetsBinding.instance.addObserver(this); + client.state?.totalUnreadCountStream?.listen((count) { + if (count > 0) { + FlutterAppBadger.updateBadgeCount(count); + } else { + FlutterAppBadger.removeBadge(); + } + }); } StreamSubscription _newMessageSubscription; diff --git a/lib/src/stream_chat_theme.dart b/lib/src/stream_chat_theme.dart index 359ab102..cc959a35 100644 --- a/lib/src/stream_chat_theme.dart +++ b/lib/src/stream_chat_theme.dart @@ -220,8 +220,8 @@ class StreamChatThemeData { return StreamChatThemeData( accentColor: accentColor, primaryColor: isDark ? Colors.black : Colors.white, - primaryIconTheme: - IconThemeData(color: isDark ? Colors.white : Colors.black), + primaryIconTheme: IconThemeData( + color: isDark ? Colors.white : Colors.black.withOpacity(.5)), defaultChannelImage: (context, channel) => SizedBox(), backgroundColor: isDark ? Colors.black : Colors.white, defaultUserImage: (context, user) => Center( @@ -287,7 +287,7 @@ class StreamChatThemeData { ), ownMessageTheme: MessageTheme( messageText: TextStyle( - fontSize: 15, + fontSize: 14.5, color: isDark ? Colors.white : Colors.black, ), createdAt: TextStyle( @@ -302,12 +302,8 @@ class StreamChatThemeData { fontSize: 12, ), messageBackgroundColor: 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), + reactionsBackgroundColor: isDark ? Colors.black : Colors.white, + reactionsBorderColor: isDark ? Color(0xff191919) : Color(0xffEAEAEA), avatarTheme: AvatarTheme( borderRadius: BorderRadius.circular(20), constraints: BoxConstraints.tightFor( @@ -320,14 +316,11 @@ class StreamChatThemeData { ), ), otherMessageTheme: MessageTheme( - ownReactionsBackgroundColor: - isDark ? Color(0xff191919) : Color(0xffEAEAEA), - ownReactionsBorderColor: isDark ? Colors.black : Colors.white, - otherReactionsBackgroundColor: isDark ? Colors.black : Colors.white, - otherReactionsBorderColor: + reactionsBackgroundColor: isDark ? Color(0xff191919) : Color(0xffEAEAEA), + reactionsBorderColor: isDark ? Colors.black : Colors.white, messageText: TextStyle( - fontSize: 15, + fontSize: 14.5, color: isDark ? Colors.white : Colors.black, ), createdAt: TextStyle( @@ -457,10 +450,8 @@ class MessageTheme { final TextStyle createdAt; final TextStyle replies; final Color messageBackgroundColor; - final Color ownReactionsBackgroundColor; - final Color ownReactionsBorderColor; - final Color otherReactionsBackgroundColor; - final Color otherReactionsBorderColor; + final Color reactionsBackgroundColor; + final Color reactionsBorderColor; final AvatarTheme avatarTheme; const MessageTheme({ @@ -469,10 +460,8 @@ class MessageTheme { this.messageAuthor, this.messageLinks, this.messageBackgroundColor, - this.ownReactionsBackgroundColor, - this.ownReactionsBorderColor, - this.otherReactionsBackgroundColor, - this.otherReactionsBorderColor, + this.reactionsBackgroundColor, + this.reactionsBorderColor, this.avatarTheme, this.createdAt, }); @@ -485,10 +474,8 @@ class MessageTheme { TextStyle replies, Color messageBackgroundColor, AvatarTheme avatarTheme, - Color ownReactionsBackgroundColor, - Color ownReactionsBorderColor, - Color otherReactionsBackgroundColor, - Color otherReactionsBorderColor, + Color reactionsBackgroundColor, + Color reactionsBorderColor, }) => MessageTheme( messageText: messageText ?? this.messageText, @@ -499,14 +486,9 @@ class MessageTheme { messageBackgroundColor ?? this.messageBackgroundColor, avatarTheme: avatarTheme ?? this.avatarTheme, replies: replies ?? this.replies, - ownReactionsBackgroundColor: - ownReactionsBackgroundColor ?? this.ownReactionsBackgroundColor, - ownReactionsBorderColor: - ownReactionsBorderColor ?? this.ownReactionsBorderColor, - otherReactionsBackgroundColor: - otherReactionsBackgroundColor ?? this.otherReactionsBackgroundColor, - otherReactionsBorderColor: - otherReactionsBorderColor ?? this.otherReactionsBorderColor, + reactionsBackgroundColor: + reactionsBackgroundColor ?? this.reactionsBackgroundColor, + reactionsBorderColor: reactionsBorderColor ?? this.reactionsBorderColor, ); } diff --git a/pubspec.yaml b/pubspec.yaml index 00b71790..d61e6874 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -11,16 +11,17 @@ environment: dependencies: flutter: sdk: flutter + flutter_app_badger: ^1.1.2 photo_view: ^0.10.1 rxdart: ^0.24.1 jiffy: ^3.0.1 flutter_portal: ^0.3.0 cached_network_image: ^2.2.0+1 - flutter_markdown: ^0.4.2 + flutter_markdown: ^0.5.0 url_launcher: ^5.4.11 - video_player: ^0.10.11+1 + video_player: ^0.10.12+5 chewie: ^0.9.10 - file_picker: ^2.0.0 + file_picker: ^2.0.8+1 image_picker: ^0.6.7+2 flutter_keyboard_visibility: ^3.2.1 stream_chat: ^0.2.10 diff --git a/test/src/message_reaction_modal_test.dart b/test/src/message_reaction_modal_test.dart index e3972780..e270793e 100644 --- a/test/src/message_reaction_modal_test.dart +++ b/test/src/message_reaction_modal_test.dart @@ -10,13 +10,21 @@ void main() { testWidgets( 'it should show one thumbs from the picker', (WidgetTester tester) async { + final client = MockClient(); + final clientState = MockClientState(); final themeData = ThemeData(); + + when(client.state).thenReturn(clientState); + when(clientState.user).thenReturn(OwnUser(id: 'user-id')); + final streamTheme = StreamChatThemeData.getDefaultTheme(themeData); + await tester.pumpWidget( MaterialApp( theme: themeData, - home: StreamChatTheme( - data: streamTheme, + home: StreamChat( + client: client, + streamChatThemeData: streamTheme, child: MessageReactionsModal( message: Message( id: 'test', diff --git a/test/src/reaction_bubble_test.dart b/test/src/reaction_bubble_test.dart index dc0381de..c524061c 100644 --- a/test/src/reaction_bubble_test.dart +++ b/test/src/reaction_bubble_test.dart @@ -1,8 +1,11 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/mockito.dart'; import 'package:stream_chat_flutter/src/reaction_bubble.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; +import 'mocks.dart'; + void main() { testWidgets( 'it should show no reactions', @@ -29,17 +32,25 @@ void main() { testWidgets( 'it should show a thumb up', (WidgetTester tester) async { + final client = MockClient(); + final clientState = MockClientState(); final themeData = ThemeData(); + + when(client.state).thenReturn(clientState); + when(clientState.user).thenReturn(OwnUser(id: 'user-id')); + await tester.pumpWidget( MaterialApp( theme: themeData, - home: StreamChatTheme( - data: StreamChatThemeData.getDefaultTheme(themeData), + home: StreamChat( + client: client, + streamChatThemeData: StreamChatThemeData.getDefaultTheme(themeData), child: Container( child: ReactionBubble( reactions: [ Reaction( type: 'thumbs_up', + user: User(id: 'test'), ), ], borderColor: Colors.black, @@ -56,20 +67,29 @@ void main() { testWidgets( 'it should show two reactions', (WidgetTester tester) async { + final client = MockClient(); + final clientState = MockClientState(); final themeData = ThemeData(); + + when(client.state).thenReturn(clientState); + when(clientState.user).thenReturn(OwnUser(id: 'user-id')); + await tester.pumpWidget( MaterialApp( theme: themeData, - home: StreamChatTheme( - data: StreamChatThemeData.getDefaultTheme(themeData), + home: StreamChat( + client: client, + streamChatThemeData: StreamChatThemeData.getDefaultTheme(themeData), child: Container( child: ReactionBubble( reactions: [ Reaction( type: 'thumbs_up', + user: User(id: 'test'), ), Reaction( type: 'love', + user: User(id: 'test'), ), ], borderColor: Colors.black,