diff --git a/example/ios/Runner/AppDelegate.swift b/example/ios/Runner/AppDelegate.swift index 69a7daf4..e024c1ef 100644 --- a/example/ios/Runner/AppDelegate.swift +++ b/example/ios/Runner/AppDelegate.swift @@ -14,6 +14,10 @@ import Flutter sharedDefaults?.removeObject(forKey: "messageQueue") } + if #available(iOS 10.0, *) { + UNUserNotificationCenter.current().delegate = self + } + GeneratedPluginRegistrant.register(with: self) return super.application(application, didFinishLaunchingWithOptions: launchOptions) } diff --git a/example/ios/Runner/Info.plist b/example/ios/Runner/Info.plist index 6c2c8e27..b41f1c19 100644 --- a/example/ios/Runner/Info.plist +++ b/example/ios/Runner/Info.plist @@ -58,5 +58,9 @@ remote-notification + UIUserInterfaceStyle + Light + UIViewControllerBasedStatusBarAppearance + diff --git a/example/test/widget_test.dart b/example/test/widget_test.dart deleted file mode 100644 index 747db1da..00000000 --- a/example/test/widget_test.dart +++ /dev/null @@ -1,30 +0,0 @@ -// This is a basic Flutter widget test. -// -// To perform an interaction with a widget in your test, use the WidgetTester -// utility that Flutter provides. For example, you can send tap and scroll -// gestures. You can also use WidgetTester to find child widgets in the widget -// tree, read text, and verify that the values of widget properties are correct. - -import 'package:flutter/material.dart'; -import 'package:flutter_test/flutter_test.dart'; - -import 'package:example/main.dart'; - -void main() { - testWidgets('Counter increments smoke test', (WidgetTester tester) async { - // Build our app and trigger a frame. - await tester.pumpWidget(MyApp()); - - // Verify that our counter starts at 0. - expect(find.text('0'), findsOneWidget); - expect(find.text('1'), findsNothing); - - // Tap the '+' icon and trigger a frame. - await tester.tap(find.byIcon(Icons.add)); - await tester.pump(); - - // Verify that our counter has incremented. - expect(find.text('0'), findsNothing); - expect(find.text('1'), findsOneWidget); - }); -} diff --git a/lib/src/channel_header.dart b/lib/src/channel_header.dart index 16aa70fc..81f95a87 100644 --- a/lib/src/channel_header.dart +++ b/lib/src/channel_header.dart @@ -78,6 +78,7 @@ class ChannelHeader extends StatelessWidget implements PreferredSizeWidget { Widget build(BuildContext context) { final channel = StreamChannel.of(context).channel; return AppBar( + brightness: Theme.of(context).brightness, elevation: 1, leading: showBackButton ? StreamBackButton(onPressed: onBackPressed) diff --git a/lib/src/message_actions_modal.dart b/lib/src/message_actions_modal.dart index 93e1c0db..00354a49 100644 --- a/lib/src/message_actions_modal.dart +++ b/lib/src/message_actions_modal.dart @@ -25,6 +25,7 @@ class MessageActionsModal extends StatelessWidget { final bool showReply; final bool reverse; final ShapeBorder messageShape; + final DisplayWidget showUserAvatar; const MessageActionsModal({ Key key, @@ -36,6 +37,7 @@ class MessageActionsModal extends StatelessWidget { this.onThreadTap, this.showCopyMessage = true, this.showReply = true, + this.showUserAvatar = DisplayWidget.show, this.editMessageInputBuilder, this.messageShape, this.reverse = false, @@ -43,7 +45,15 @@ class MessageActionsModal extends StatelessWidget { @override Widget build(BuildContext context) { - final ownId = StreamChat.of(context).user.id; + var size = MediaQuery.of(context).size; + var user = StreamChat.of(context).user; + + var roughMaxSize = 2 * size.width / 3; + var roughSentenceSize = + message.text.length * messageTheme.messageText.fontSize * 1.2; + var divFactor = + roughSentenceSize == 0 ? 1 : (roughSentenceSize / roughMaxSize); + return GestureDetector( behavior: HitTestBehavior.translucent, onTap: () { @@ -58,7 +68,7 @@ class MessageActionsModal extends StatelessWidget { sigmaY: 10.8731, ), child: Container( - color: Colors.black.withOpacity(0.2), + color: Colors.black.withOpacity(0.1), ), ), ), @@ -73,7 +83,12 @@ class MessageActionsModal extends StatelessWidget { if (showReactions && (message.status == MessageSendingStatus.SENT || message.status == null)) - Center( + Align( + alignment: Alignment( + user.id == message.user.id + ? (divFactor > 1.0 ? 0.0 : (1.0 - divFactor)) + : (divFactor > 1.0 ? 0.0 : -(1.0 - divFactor)), + 0.0), child: ReactionPicker( message: message, messageTheme: messageTheme, @@ -92,10 +107,10 @@ class MessageActionsModal extends StatelessWidget { showReactions: false, showUsername: false, showReplyIndicator: false, - showUserAvatar: message.user.id == ownId - ? DisplayWidget.gone - : DisplayWidget.show, + showUserAvatar: showUserAvatar, showTimestamp: false, + translateUserAvatar: false, + showReactionPickerIndicator: true, showSendingIndicator: DisplayWidget.gone, shape: messageShape, ), diff --git a/lib/src/message_reactions_modal.dart b/lib/src/message_reactions_modal.dart index 375da94e..fc3f68a4 100644 --- a/lib/src/message_reactions_modal.dart +++ b/lib/src/message_reactions_modal.dart @@ -17,6 +17,7 @@ class MessageReactionsModal extends StatelessWidget { final MessageTheme messageTheme; final bool reverse; final bool showReactions; + final DisplayWidget showUserAvatar; final ShapeBorder messageShape; final void Function(User) onUserAvatarTap; @@ -29,12 +30,21 @@ class MessageReactionsModal extends StatelessWidget { this.editMessageInputBuilder, this.messageShape, this.reverse = false, + this.showUserAvatar = DisplayWidget.show, this.onUserAvatarTap, }) : super(key: key); @override Widget build(BuildContext context) { - final ownId = StreamChat.of(context).user.id; + var size = MediaQuery.of(context).size; + var user = StreamChat.of(context).user; + + var roughMaxSize = 2 * size.width / 3; + var roughSentenceSize = + message.text.length * messageTheme.messageText.fontSize * 1.2; + var divFactor = + roughSentenceSize == 0 ? 1 : (roughSentenceSize / roughMaxSize); + return GestureDetector( behavior: HitTestBehavior.translucent, onTap: () { @@ -49,7 +59,7 @@ class MessageReactionsModal extends StatelessWidget { sigmaY: 10.8731, ), child: Container( - color: Colors.black.withOpacity(0.2), + color: Colors.black.withOpacity(0.1), ), ), ), @@ -64,7 +74,12 @@ class MessageReactionsModal extends StatelessWidget { if (showReactions && (message.status == MessageSendingStatus.SENT || message.status == null)) - Center( + Align( + alignment: Alignment( + user.id == message.user.id + ? (divFactor > 1.0 ? 0.0 : (1.0 - divFactor)) + : (divFactor > 1.0 ? 0.0 : -(1.0 - divFactor)), + 0.0), child: ReactionPicker( message: message, messageTheme: messageTheme, @@ -82,13 +97,13 @@ class MessageReactionsModal extends StatelessWidget { messageTheme: messageTheme, showReactions: false, showUsername: false, - showUserAvatar: message.user.id == ownId - ? DisplayWidget.gone - : DisplayWidget.show, + showUserAvatar: showUserAvatar, showReplyIndicator: false, showTimestamp: false, + translateUserAvatar: false, showSendingIndicator: DisplayWidget.gone, shape: messageShape, + showReactionPickerIndicator: true, ), ), SizedBox( diff --git a/lib/src/message_widget.dart b/lib/src/message_widget.dart index 0e703fa0..e65da576 100644 --- a/lib/src/message_widget.dart +++ b/lib/src/message_widget.dart @@ -106,6 +106,9 @@ class MessageWidget extends StatefulWidget { /// The function called when tapping on a link final void Function(String) onLinkTap; + /// Used in [MessageReactionsModal] and [MessageActionsModal] + final bool showReactionPickerIndicator; + final List readList; /// If true show the users username next to the timestamp of the message @@ -115,11 +118,15 @@ class MessageWidget extends StatefulWidget { final bool showEditMessage; final Map attachmentBuilders; + /// Center user avatar with bottom of the message + final bool translateUserAvatar; + MessageWidget({ Key key, @required this.message, @required this.messageTheme, this.reverse = false, + this.translateUserAvatar = true, this.shape, this.attachmentShape, this.borderSide, @@ -127,6 +134,7 @@ class MessageWidget extends StatefulWidget { this.borderRadiusGeometry, this.attachmentBorderRadiusGeometry, this.onMentionTap, + this.showReactionPickerIndicator = false, this.showUserAvatar = DisplayWidget.show, this.showSendingIndicator = DisplayWidget.show, this.showReplyIndicator = true, @@ -247,39 +255,62 @@ class _MessageWidgetState extends State { ), 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) - ? Transform( - alignment: Alignment.center, + child: Stack( + clipBehavior: Clip.none, + children: [ + 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) + ? Transform( + alignment: Alignment.center, + transform: Matrix4.rotationY( + widget.reverse ? pi : 0), + child: DeletedMessage( + messageTheme: widget.messageTheme, + ), + ) + : Column( + crossAxisAlignment: + CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + ..._parseAttachments(context), + if (widget.message.text + .trim() + .isNotEmpty) + _buildTextBubble(context), + ], + ), + ), + if (widget.showReactionPickerIndicator) + Positioned( + right: 0, + top: -6, + child: Transform( transform: Matrix4.rotationY( widget.reverse ? pi : 0), - child: DeletedMessage( - messageTheme: widget.messageTheme, + child: CustomPaint( + painter: ReactionBubblePainter( + widget.messageTheme + .reactionsBackgroundColor, + widget.messageTheme + .reactionsBorderColor, + ), ), - ) - : Column( - crossAxisAlignment: - CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - ..._parseAttachments(context), - if (widget.message.text - .trim() - .isNotEmpty) - _buildTextBubble(context), - ], ), + ), + ], ), ), ), @@ -423,6 +454,10 @@ class _MessageWidgetState extends State { return StreamChannel( channel: channel, child: MessageActionsModal( + showUserAvatar: + widget.message.user.id == channel.client.state.user.id + ? DisplayWidget.gone + : DisplayWidget.show, messageTheme: widget.messageTheme, messageShape: widget.shape ?? _getDefaultShape(context), reverse: widget.reverse, @@ -447,6 +482,10 @@ class _MessageWidgetState extends State { return StreamChannel( channel: channel, child: MessageReactionsModal( + showUserAvatar: + widget.message.user.id == channel.client.state.user.id + ? DisplayWidget.gone + : DisplayWidget.show, onUserAvatarTap: widget.onUserAvatarTap, messageTheme: widget.messageTheme, messageShape: widget.shape ?? _getDefaultShape(context), @@ -614,7 +653,11 @@ class _MessageWidgetState extends State { padding: const EdgeInsets.symmetric(horizontal: 4.0), child: Transform.translate( offset: Offset( - 0, widget.messageTheme.avatarTheme.constraints.maxHeight / 2), + 0, + widget.translateUserAvatar + ? widget.messageTheme.avatarTheme.constraints.maxHeight / 2 + : 0, + ), child: UserAvatar( user: widget.message.user, onTap: widget.onUserAvatarTap, diff --git a/lib/src/reaction_picker.dart b/lib/src/reaction_picker.dart index 87487cde..e7d302e6 100644 --- a/lib/src/reaction_picker.dart +++ b/lib/src/reaction_picker.dart @@ -1,5 +1,4 @@ import 'package:flutter/material.dart'; -import 'package:stream_chat_flutter/src/reaction_bubble.dart'; import '../stream_chat_flutter.dart'; @@ -22,59 +21,44 @@ class ReactionPicker extends StatelessWidget { @override Widget build(BuildContext context) { final reactionIcons = StreamChatTheme.of(context).reactionIcons; - return Stack( - fit: StackFit.passthrough, - children: [ - Material( - color: messageTheme.reactionsBackgroundColor, - 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.withOpacity(.5), - ), - 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.reactionsBackgroundColor, - messageTheme.reactionsBorderColor, + return Material( + color: messageTheme.reactionsBackgroundColor, + clipBehavior: Clip.hardEdge, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(24), + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.end, + 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.withOpacity(.5), ), - ), - ), - ], + onPressed: () { + if (ownReactionIndex != -1) { + removeReaction( + context, + message.ownReactions[ownReactionIndex], + ); + } else { + sendReaction( + context, + reactionIcon.type, + ); + } + }, + ); + }).toList(), + ), ); } diff --git a/pubspec.yaml b/pubspec.yaml index d61e6874..452eb69a 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -24,7 +24,7 @@ dependencies: file_picker: ^2.0.8+1 image_picker: ^0.6.7+2 flutter_keyboard_visibility: ^3.2.1 - stream_chat: ^0.2.10 + stream_chat: ^0.2.10+1 mime: ^0.9.6+3 visibility_detector: ^0.1.5 http_parser: ^3.1.4