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/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_list_view.dart b/lib/src/message_list_view.dart index 7c529f95..bb0f98c0 100644 --- a/lib/src/message_list_view.dart +++ b/lib/src/message_list_view.dart @@ -6,6 +6,7 @@ import 'package:stream_chat/stream_chat.dart'; import 'package:stream_chat_flutter/src/message_widget.dart'; import 'package:stream_chat_flutter/src/system_message.dart'; import 'package:visibility_detector/visibility_detector.dart'; +import 'package:widgets_visibility_provider/widgets_visibility_provider.dart'; import '../stream_chat_flutter.dart'; import 'date_divider.dart'; @@ -155,144 +156,189 @@ class _MessageListViewState extends State { final streamChannel = StreamChannel.of(context); /// TODO: find a better solution when (https://github.com/flutter/flutter/issues/21023) is fixed - return Stack( - children: [ - NotificationListener( - onNotification: (_) { - if (_scrollController.offset < 150 && _newMessageList.isNotEmpty) { - setState(() { - _messages.insertAll(0, _newMessageList); - _newMessageList.clear(); - }); - } - return true; - }, - child: ListView.custom( - key: Key('messageListView'), - physics: widget.scrollPhysics, - keyboardDismissBehavior: widget.keyboardDismissBehavior, - controller: _scrollController, - reverse: true, - childrenDelegate: SliverChildBuilderDelegate( - (context, i) { - if (i == _messages.length + 1) { - if (widget.parentMessage != null) { - if (widget.parentMessageBuilder != null) { - return widget.parentMessageBuilder( - context, - widget.parentMessage, - ); - } else { - return Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - buildParentMessage(widget.parentMessage), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 32), - child: Container( - padding: const EdgeInsets.all(8), - child: Text( - 'Start of thread', - textAlign: TextAlign.center, - ), - color: - Theme.of(context).accentColor.withAlpha(50), - ), - ), - ], - ); - } - } else { - return SizedBox(); - } - } - - if (i == _messages.length) { - return _buildLoadingIndicator(streamChannel); - } - final message = _messages[i]; - final nextMessage = i > 0 ? _messages[i - 1] : null; - - Widget messageWidget; - - if (i == 0) { - messageWidget = _buildBottomMessage( - context, - message, - _messages, - streamChannel, - ); - } else if (i == _messages.length - 1) { - messageWidget = _buildTopMessage( - context, - message, - _messages, - streamChannel, - ); - } else { - if (widget.messageBuilder != null) { - messageWidget = Builder( - key: ValueKey('MESSAGE-${message.id}'), - builder: (_) => widget.messageBuilder( + return WidgetsVisibilityProvider( + condition: (positionData) => + positionData.endPosition >= 20 && + positionData.startPosition <= positionData.viewportSize, + child: Stack( + alignment: Alignment.center, + children: [ + NotificationListener( + onNotification: (_) { + if (_scrollController.offset < 150 && + _newMessageList.isNotEmpty) { + setState(() { + _messages.insertAll(0, _newMessageList); + _newMessageList.clear(); + }); + } + return true; + }, + child: ListView.custom( + key: Key('messageListView'), + physics: widget.scrollPhysics, + keyboardDismissBehavior: widget.keyboardDismissBehavior, + controller: _scrollController, + reverse: true, + childrenDelegate: SliverChildBuilderDelegate( + (context, i) { + if (i == _messages.length + 1) { + if (widget.parentMessage != null) { + if (widget.parentMessageBuilder != null) { + return widget.parentMessageBuilder( context, - MessageDetails( - context, - message, - _messages, - i, - ), - _messages), + widget.parentMessage, + ); + } else { + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + buildParentMessage(widget.parentMessage), + Padding( + padding: + const EdgeInsets.symmetric(horizontal: 32), + child: Container( + padding: const EdgeInsets.all(8), + child: Text( + 'Start of thread', + textAlign: TextAlign.center, + ), + color: + Theme.of(context).accentColor.withAlpha(50), + ), + ), + ], + ); + } + } else { + return SizedBox(); + } + } + + if (i == _messages.length) { + return _buildLoadingIndicator(streamChannel); + } + final message = _messages[i]; + final nextMessage = i > 0 ? _messages[i - 1] : null; + + Widget messageWidget; + + if (i == 0) { + messageWidget = _buildBottomMessage( + context, + message, + _messages, + streamChannel, + ); + } else if (i == _messages.length - 1) { + messageWidget = _buildTopMessage( + context, + message, + _messages, + streamChannel, ); } else { - messageWidget = buildMessage(message, _messages, i); + if (widget.messageBuilder != null) { + messageWidget = Builder( + key: ValueKey('MESSAGE-${message.id}'), + builder: (_) => widget.messageBuilder( + context, + MessageDetails( + context, + message, + _messages, + i, + ), + _messages), + ); + } else { + messageWidget = buildMessage(message, _messages, i); + } } - } - if (nextMessage != null && - !Jiffy(message.createdAt.toLocal()) - .isSame(nextMessage.createdAt.toLocal(), Units.DAY)) { - return Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - messageWidget, - Padding( - padding: const EdgeInsets.symmetric(vertical: 12.0), - child: widget.dateDividerBuilder != null - ? widget.dateDividerBuilder( - nextMessage.createdAt.toLocal()) - : DateDivider( - dateTime: nextMessage.createdAt.toLocal(), - ), - ), - ], + if (nextMessage != null && + !Jiffy(message.createdAt.toLocal()) + .isSame(nextMessage.createdAt.toLocal(), Units.DAY)) { + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + messageWidget, + Padding( + padding: const EdgeInsets.symmetric(vertical: 12.0), + child: VisibleNotifierWidget( + condition: ( + ScrollNotification previousNotification, + PositionData previousPositionData, + ScrollNotification currentNotification, + PositionData currentPositionData, + ) { + print( + 'currentPositionData.endPosition: ${currentPositionData.endPosition}'); + return true; + }, + data: i, + child: widget.dateDividerBuilder != null + ? widget.dateDividerBuilder( + nextMessage.createdAt.toLocal()) + : DateDivider( + dateTime: nextMessage.createdAt.toLocal(), + ), + ), + ), + ], + ); + } + + return VisibleNotifierWidget( + child: messageWidget, + data: i, ); + }, + childCount: _messages.length + 2, + findChildIndexCallback: (key) { + final ValueKey valueKey = key; + final index = _messages + .indexWhere((m) => 'MESSAGE-${m.id}' == valueKey.value); + return index != -1 ? index : null; + }, + ), + ), + ), + if (widget.showScrollToBottom) + StreamBuilder( + stream: streamChannel.channel.on( + EventType.messageNew, + ), + builder: (context, _) { + if (!_showScrollToBottom || + streamChannel.channel.state.unreadCount == 0) { + return SizedBox(); + } + return _buildScrollToBottom(streamChannel); + }), + Positioned( + top: 20.0, + child: WidgetsVisibilityConsumer( + listener: ( + context, + event, + ) {}, + builder: (context, event) { + if (event.positionDataList == null || + event.positionDataList.isEmpty) { + return Container(); } - return messageWidget; - }, - childCount: _messages.length + 2, - findChildIndexCallback: (key) { - final ValueKey valueKey = key; - final index = _messages - .indexWhere((m) => 'MESSAGE-${m.id}' == valueKey.value); - return index != -1 ? index : null; + return DateDivider( + dateTime: _messages[event.positionDataList.last.data] + .createdAt + .toLocal(), + ); }, ), ), - ), - if (widget.showScrollToBottom) - StreamBuilder( - stream: streamChannel.channel.on( - EventType.messageNew, - ), - builder: (context, _) { - if (!_showScrollToBottom || - streamChannel.channel.state.unreadCount == 0) { - return SizedBox(); - } - return _buildScrollToBottom(streamChannel); - }), - ], + ], + ), ); } 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_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 c3b971a7..4231dab2 100644 --- a/lib/src/stream_chat.dart +++ b/lib/src/stream_chat.dart @@ -130,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, @@ -151,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, 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 7c92406d..548dc624 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -31,6 +31,7 @@ dependencies: flutter_slidable: ^0.5.4 carousel_slider: ^2.2.1 clipboard: ^0.1.2+8 + widgets_visibility_provider: ^2.0.2 flutter: fonts: