diff --git a/packages/stream_chat_flutter/lib/src/deleted_message.dart b/packages/stream_chat_flutter/lib/src/deleted_message.dart index 93e70b12..9a4e0ae2 100644 --- a/packages/stream_chat_flutter/lib/src/deleted_message.dart +++ b/packages/stream_chat_flutter/lib/src/deleted_message.dart @@ -1,5 +1,3 @@ -import 'dart:math'; - import 'package:flutter/material.dart'; import 'package:stream_chat_flutter/src/stream_chat_theme.dart'; @@ -33,36 +31,28 @@ class DeletedMessage extends StatelessWidget { @override Widget build(BuildContext context) { final chatThemeData = StreamChatTheme.of(context); - return Transform( - transform: Matrix4.rotationY(reverse ? pi : 0), - alignment: Alignment.center, - child: Material( - color: messageTheme.messageBackgroundColor, - shape: shape ?? - RoundedRectangleBorder( - borderRadius: borderRadiusGeometry ?? BorderRadius.zero, - side: borderSide ?? - BorderSide( - color: Theme.of(context).brightness == Brightness.dark - ? chatThemeData.colorTheme.white.withAlpha(24) - : chatThemeData.colorTheme.black.withAlpha(24), - ), - ), - child: Padding( - padding: const EdgeInsets.symmetric( - vertical: 8, - horizontal: 16, + return Material( + color: messageTheme.messageBackgroundColor, + shape: shape ?? + RoundedRectangleBorder( + borderRadius: borderRadiusGeometry ?? BorderRadius.zero, + side: borderSide ?? + BorderSide( + color: Theme.of(context).brightness == Brightness.dark + ? chatThemeData.colorTheme.white.withAlpha(24) + : chatThemeData.colorTheme.black.withAlpha(24), + ), ), - child: Transform( - transform: Matrix4.rotationY(reverse ? pi : 0), - alignment: Alignment.center, - child: Text( - 'Message deleted', - style: messageTheme.messageText?.copyWith( - fontStyle: FontStyle.italic, - color: messageTheme.createdAt?.color, - ), - ), + child: Padding( + padding: const EdgeInsets.symmetric( + vertical: 8, + horizontal: 16, + ), + child: Text( + 'Message deleted', + style: messageTheme.messageText?.copyWith( + fontStyle: FontStyle.italic, + color: messageTheme.createdAt?.color, ), ), ), diff --git a/packages/stream_chat_flutter/lib/src/extension.dart b/packages/stream_chat_flutter/lib/src/extension.dart index a65272c3..3b6ba3e2 100644 --- a/packages/stream_chat_flutter/lib/src/extension.dart +++ b/packages/stream_chat_flutter/lib/src/extension.dart @@ -104,3 +104,15 @@ extension BuildContextX on BuildContext { double get textScaleFactor => MediaQuery.maybeOf(this)?.textScaleFactor ?? 1.0; } + +/// Extension on [BorderRadius] +extension FlipBorder on BorderRadius { + /// Flips borders (Y) + BorderRadius mirrorBorderIfReversed({bool reverse = true}) => reverse + ? BorderRadius.only( + topLeft: topRight, + topRight: topLeft, + bottomLeft: bottomRight, + bottomRight: bottomLeft) + : this; +} diff --git a/packages/stream_chat_flutter/lib/src/message_actions_modal.dart b/packages/stream_chat_flutter/lib/src/message_actions_modal.dart index 82ce94e2..5336ed48 100644 --- a/packages/stream_chat_flutter/lib/src/message_actions_modal.dart +++ b/packages/stream_chat_flutter/lib/src/message_actions_modal.dart @@ -173,12 +173,12 @@ class _MessageActionsModalState extends State { Align( alignment: Alignment( user?.id == widget.message.user?.id - ? (divFactor > 1.0 - ? 0.0 - : (1.0 - divFactor)) - : (divFactor > 1.0 - ? 0.0 - : -(1.0 - divFactor)), + ? (divFactor >= 1.0 + ? -0.2 + : (1.2 - divFactor)) + : (divFactor >= 1.0 + ? 0.2 + : -(1.2 - divFactor)), 0), child: ReactionPicker( message: widget.message, @@ -189,8 +189,10 @@ class _MessageActionsModalState extends State { child: MessageWidget( key: const Key('MessageWidget'), reverse: widget.reverse, - attachmentBorderRadiusGeometry: - widget.attachmentBorderRadiusGeometry, + attachmentBorderRadiusGeometry: widget + .attachmentBorderRadiusGeometry + ?.mirrorBorderIfReversed( + reverse: !widget.reverse), message: widget.message.copyWith( text: widget.message.text!.length > 200 // ignore: lines_longer_than_80_chars diff --git a/packages/stream_chat_flutter/lib/src/message_list_view.dart b/packages/stream_chat_flutter/lib/src/message_list_view.dart index a460eaa5..c24ceb00 100644 --- a/packages/stream_chat_flutter/lib/src/message_list_view.dart +++ b/packages/stream_chat_flutter/lib/src/message_list_view.dart @@ -980,25 +980,44 @@ class _MessageListViewState extends State { onReplyTap: widget.onReplyTap, attachmentBorderRadiusGeometry: BorderRadius.only( topLeft: Radius.circular(attachmentBorderRadius), - bottomLeft: Radius.circular( - (timeDiff >= 1 || !isNextUserSame) && - !(hasReplies || isThreadMessage || hasFileAttachment) - ? 0 - : attachmentBorderRadius, - ), + bottomLeft: isMyMessage + ? Radius.circular(attachmentBorderRadius) + : Radius.circular( + (timeDiff >= 1 || !isNextUserSame) && + !(hasReplies || isThreadMessage || hasFileAttachment) + ? 0 + : attachmentBorderRadius, + ), topRight: Radius.circular(attachmentBorderRadius), - bottomRight: Radius.circular(attachmentBorderRadius), + bottomRight: isMyMessage + ? Radius.circular( + (timeDiff >= 1 || !isNextUserSame) && + !(hasReplies || isThreadMessage || hasFileAttachment) + ? 0 + : attachmentBorderRadius, + ) + : Radius.circular(attachmentBorderRadius), ), attachmentPadding: EdgeInsets.all(hasFileAttachment ? 4 : 2), borderRadiusGeometry: BorderRadius.only( topLeft: const Radius.circular(16), - bottomLeft: Radius.circular( - (timeDiff >= 1 || !isNextUserSame) && !(hasReplies || isThreadMessage) - ? 0 - : 16, - ), + bottomLeft: isMyMessage + ? const Radius.circular(16) + : Radius.circular( + (timeDiff >= 1 || !isNextUserSame) && + !(hasReplies || isThreadMessage) + ? 0 + : 16, + ), topRight: const Radius.circular(16), - bottomRight: const Radius.circular(16), + bottomRight: isMyMessage + ? Radius.circular( + (timeDiff >= 1 || !isNextUserSame) && + !(hasReplies || isThreadMessage) + ? 0 + : 16, + ) + : const Radius.circular(16), ), textPadding: EdgeInsets.symmetric( vertical: 8, diff --git a/packages/stream_chat_flutter/lib/src/message_reactions_modal.dart b/packages/stream_chat_flutter/lib/src/message_reactions_modal.dart index 76ce215c..5f3bc5a4 100644 --- a/packages/stream_chat_flutter/lib/src/message_reactions_modal.dart +++ b/packages/stream_chat_flutter/lib/src/message_reactions_modal.dart @@ -112,12 +112,12 @@ class MessageReactionsModal extends StatelessWidget { Align( alignment: Alignment( user!.id == message.user!.id - ? (divFactor > 1.0 - ? 0.0 - : (1.0 - divFactor)) - : (divFactor > 1.0 - ? 0.0 - : -(1.0 - divFactor)), + ? (divFactor >= 1.0 + ? -0.2 + : (1.2 - divFactor)) + : (divFactor >= 1.0 + ? 0.2 + : -(1.2 - divFactor)), 0), child: ReactionPicker( message: message, @@ -144,7 +144,9 @@ class MessageReactionsModal extends StatelessWidget { attachmentShape: attachmentShape, padding: const EdgeInsets.all(0), attachmentBorderRadiusGeometry: - attachmentBorderRadiusGeometry, + attachmentBorderRadiusGeometry + ?.mirrorBorderIfReversed( + reverse: !reverse), attachmentPadding: EdgeInsets.all( hasFileAttachment ? 4 : 2, ), diff --git a/packages/stream_chat_flutter/lib/src/message_widget.dart b/packages/stream_chat_flutter/lib/src/message_widget.dart index fc25769d..97e061ae 100644 --- a/packages/stream_chat_flutter/lib/src/message_widget.dart +++ b/packages/stream_chat_flutter/lib/src/message_widget.dart @@ -1,4 +1,3 @@ -import 'dart:math'; import 'dart:ui'; import 'package:flutter/cupertino.dart'; @@ -131,8 +130,6 @@ class MessageWidget extends StatefulWidget { ), border, reverse, - attachmentBorderRadiusGeometry as BorderRadius? ?? - BorderRadius.zero, ), ); } @@ -157,8 +154,6 @@ class MessageWidget extends StatefulWidget { ), border, reverse, - attachmentBorderRadiusGeometry as BorderRadius? ?? - BorderRadius.zero, ); }, 'video': (context, message, attachments) { @@ -191,8 +186,6 @@ class MessageWidget extends StatefulWidget { ), border, reverse, - attachmentBorderRadiusGeometry as BorderRadius? ?? - BorderRadius.zero, ); }, 'giphy': (context, message, attachments) { @@ -219,8 +212,6 @@ class MessageWidget extends StatefulWidget { ), border, reverse, - attachmentBorderRadiusGeometry as BorderRadius? ?? - BorderRadius.zero, ); }, 'file': (context, message, attachments) { @@ -248,8 +239,6 @@ class MessageWidget extends StatefulWidget { ), border, reverse, - attachmentBorderRadiusGeometry as BorderRadius? ?? - BorderRadius.zero, ); }) .insertBetween(SizedBox( @@ -472,184 +461,176 @@ class _MessageWidgetState extends State : () => onLongPress(context), child: Padding( padding: widget.padding ?? const EdgeInsets.all(8), - child: Transform( - alignment: Alignment.center, - transform: Matrix4.rotationY(widget.reverse ? pi : 0), - child: FractionallySizedBox( - alignment: Alignment.centerLeft, - widthFactor: 0.78, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - Stack( - clipBehavior: Clip.none, - alignment: AlignmentDirectional.bottomStart, - children: [ - Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - Row( - crossAxisAlignment: CrossAxisAlignment.end, - mainAxisSize: MainAxisSize.min, - children: [ - if (widget.showUserAvatar == - DisplayWidget.show && - widget.message.user != null) ...[ - _buildUserAvatar(), - const SizedBox(width: 4), - ], - if (widget.showUserAvatar == DisplayWidget.hide) - SizedBox(width: avatarWidth + 4), - Flexible( - child: PortalEntry( - portal: Container( - transform: - Matrix4.translationValues(-12, 0, 0), - constraints: const BoxConstraints( - maxWidth: 22 * 6.0), - child: _buildReactionIndicator(context), - ), - portalAnchor: const Alignment(-1, -1), - childAnchor: const Alignment(1, -1), - 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 && - !isFailedState) - ? Transform( - alignment: Alignment.center, - transform: Matrix4.rotationY( - widget.reverse ? pi : 0), - child: Container( - // ignore: lines_longer_than_80_chars - margin: EdgeInsets.symmetric( - horizontal: - // ignore: lines_longer_than_80_chars - widget.showUserAvatar == - // ignore: lines_longer_than_80_chars - DisplayWidget.gone - ? 0 - : 4.0), - child: DeletedMessage( - reverse: widget.reverse, - // ignore: lines_longer_than_80_chars - borderRadiusGeometry: widget - .borderRadiusGeometry, - borderSide: - widget.borderSide, - shape: widget.shape, - messageTheme: - widget.messageTheme, - ), - ), - ) - : Card( - clipBehavior: Clip.antiAlias, - elevation: 0, - margin: EdgeInsets.symmetric( - horizontal: (isFailedState - ? 15.0 - : 0.0) + + child: FractionallySizedBox( + alignment: + widget.reverse ? Alignment.centerRight : Alignment.centerLeft, + widthFactor: 0.78, + child: Column( + crossAxisAlignment: widget.reverse + ? CrossAxisAlignment.end + : CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Stack( + clipBehavior: Clip.none, + alignment: widget.reverse + ? AlignmentDirectional.bottomEnd + : AlignmentDirectional.bottomStart, + children: [ + Column( + crossAxisAlignment: widget.reverse + ? CrossAxisAlignment.end + : CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.end, + mainAxisSize: MainAxisSize.min, + children: [ + if (widget.showUserAvatar == DisplayWidget.show && + widget.message.user != null) ...[ + _buildUserAvatar(), + const SizedBox(width: 4), + ], + if (widget.showUserAvatar == DisplayWidget.hide) + SizedBox(width: avatarWidth + 4), + Flexible( + child: PortalEntry( + portal: Container( + transform: Matrix4.translationValues( + widget.reverse ? 12 : -12, 0, 0), + constraints: const BoxConstraints( + maxWidth: 22 * 6.0), + child: _buildReactionIndicator(context), + ), + portalAnchor: + Alignment(widget.reverse ? 1 : -1, -1), + childAnchor: + Alignment(widget.reverse ? -1 : 1, -1), + 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 && + !isFailedState) + ? Container( + // ignore: lines_longer_than_80_chars + margin: EdgeInsets.symmetric( + horizontal: // ignore: lines_longer_than_80_chars - (widget.showUserAvatar == + widget.showUserAvatar == + // ignore: lines_longer_than_80_chars DisplayWidget .gone ? 0 : 4.0), - ), - shape: widget.shape ?? - RoundedRectangleBorder( - side: widget - .borderSide ?? - BorderSide( - color: widget - // ignore: lines_longer_than_80_chars - .messageTheme - // ignore: lines_longer_than_80_chars - .messageBorderColor ?? - Colors.grey, - ), - borderRadius: widget - // ignore: lines_longer_than_80_chars - .borderRadiusGeometry ?? - BorderRadius.zero, - ), - color: _getBackgroundColor(), - child: Column( - crossAxisAlignment: - CrossAxisAlignment.end, - mainAxisSize: - MainAxisSize.min, - children: [ - if (hasQuotedMessage) - _buildQuotedMessage(), - if (hasNonUrlAttachments) - _parseAttachments(), - if (!isGiphy) - _buildTextBubble(), - ], - ), + child: DeletedMessage( + borderRadiusGeometry: widget + .borderRadiusGeometry, + borderSide: widget.borderSide, + shape: widget.shape, + messageTheme: + widget.messageTheme, ), - ), - if (widget.showReactionPickerIndicator) - Positioned( - right: 4, - top: -8, - child: Transform( - transform: Matrix4.rotationY( - widget.reverse ? pi : 0), - child: CustomPaint( - painter: ReactionBubblePainter( - StreamChatTheme.of(context) - .colorTheme - .white, - Colors.transparent, - Colors.transparent, - tailCirclesSpace: 1, + ) + : Card( + clipBehavior: Clip.antiAlias, + elevation: 0, + margin: EdgeInsets.symmetric( + horizontal: (isFailedState + ? 15.0 + : 0.0) + + // ignore: lines_longer_than_80_chars + (widget.showUserAvatar == + DisplayWidget.gone + ? 0 + : 4.0), + ), + shape: widget.shape ?? + RoundedRectangleBorder( + side: widget.borderSide ?? + BorderSide( + color: widget + // ignore: lines_longer_than_80_chars + .messageTheme + // ignore: lines_longer_than_80_chars + .messageBorderColor ?? + Colors.grey, + ), + borderRadius: widget + // ignore: lines_longer_than_80_chars + .borderRadiusGeometry ?? + BorderRadius.zero, + ), + color: _getBackgroundColor(), + child: Column( + crossAxisAlignment: + CrossAxisAlignment.end, + mainAxisSize: + MainAxisSize.min, + children: [ + if (hasQuotedMessage) + _buildQuotedMessage(), + if (hasNonUrlAttachments) + _parseAttachments(), + if (!isGiphy) + _buildTextBubble(), + ], ), ), + ), + if (widget.showReactionPickerIndicator) + Positioned( + right: widget.reverse ? null : 4, + left: widget.reverse ? 4 : null, + top: -8, + child: CustomPaint( + painter: ReactionBubblePainter( + StreamChatTheme.of(context) + .colorTheme + .white, + Colors.transparent, + Colors.transparent, + tailCirclesSpace: 1, ), ), - ], - ), + ), + ], ), ), - ], - ), - if (showBottomRow) - SizedBox(height: context.textScaleFactor * 18.0), - ], + ), + ], + ), + if (showBottomRow) + SizedBox(height: context.textScaleFactor * 18.0), + ], + ), + if (showBottomRow) + Padding( + padding: EdgeInsets.only(left: leftPadding), + child: _bottomRow, ), - if (showBottomRow) - Padding( - padding: EdgeInsets.only(left: leftPadding), - child: _bottomRow, - ), - if (isFailedState) - Positioned( - left: widget.reverse ? 0 : null, - right: widget.reverse ? null : 0, - bottom: showBottomRow ? 18 : -2, - child: StreamSvgIcon.error(size: 20), - ), - ], - ), - ], - ), + if (isFailedState) + Positioned( + left: widget.reverse ? 0 : null, + right: widget.reverse ? null : 0, + bottom: showBottomRow ? 18 : -2, + child: StreamSvgIcon.error(size: 20), + ), + ], + ), + ], ), ), ), @@ -681,24 +662,20 @@ class _MessageWidgetState extends State Widget get _bottomRow { if (isDeleted) { final chatThemeData = StreamChatTheme.of(context); - return Transform( - transform: Matrix4.rotationY(widget.reverse ? pi : 0), - alignment: Alignment.center, - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - StreamSvgIcon.eye( - color: chatThemeData.colorTheme.grey, - size: 16, - ), - const SizedBox(width: 8), - Text( - 'Only visible to you', - style: chatThemeData.textTheme.footnote - .copyWith(color: chatThemeData.colorTheme.grey), - ), - ], - ), + return Row( + mainAxisSize: MainAxisSize.min, + children: [ + StreamSvgIcon.eye( + color: chatThemeData.colorTheme.grey, + size: 16, + ), + const SizedBox(width: 8), + Text( + 'Only visible to you', + style: chatThemeData.textTheme.footnote + .copyWith(color: chatThemeData.colorTheme.grey), + ), + ], ); } @@ -765,8 +742,10 @@ class _MessageWidgetState extends State return Row( crossAxisAlignment: CrossAxisAlignment.end, + mainAxisAlignment: + widget.reverse ? MainAxisAlignment.end : MainAxisAlignment.start, children: [ - if (showThreadTail) + if (showThreadTail && !widget.reverse) Container( margin: EdgeInsets.only( bottom: context.textScaleFactor * @@ -777,18 +756,15 @@ class _MessageWidgetState extends State painter: _ThreadReplyPainter( context: context, color: widget.messageTheme.messageBorderColor, + reverse: widget.reverse, ), ), ), ...children.map( (child) { - Widget mappedChild = Transform( - transform: Matrix4.rotationY(widget.reverse ? pi : 0), - alignment: Alignment.center, - child: SizedBox( - height: context.textScaleFactor * 14, - child: child, - ), + Widget mappedChild = SizedBox( + height: context.textScaleFactor * 14, + child: child, ); if (child.key == usernameKey) { mappedChild = Flexible(child: mappedChild); @@ -796,6 +772,21 @@ class _MessageWidgetState extends State return mappedChild; }, ), + if (showThreadTail && widget.reverse) + Container( + margin: EdgeInsets.only( + bottom: context.textScaleFactor * + ((widget.messageTheme.replies?.fontSize ?? 1) / 2), + ), + child: CustomPaint( + size: const Size(16, 32) * context.textScaleFactor, + painter: _ThreadReplyPainter( + context: context, + color: widget.messageTheme.messageBorderColor, + reverse: widget.reverse, + ), + ), + ), ].insertBetween(const SizedBox(width: 8)), ); } @@ -1078,56 +1069,47 @@ class _MessageWidgetState extends State return child; } - Widget _buildUserAvatar() => Transform( - transform: Matrix4.rotationY(widget.reverse ? pi : 0), - alignment: Alignment.center, - child: Transform.translate( - offset: Offset( - 0, - widget.translateUserAvatar - ? (widget.messageTheme.avatarTheme?.constraints.maxHeight ?? - 40) / - 2 - : 0, - ), - child: UserAvatar( - user: widget.message.user!, - onTap: widget.onUserAvatarTap, - constraints: widget.messageTheme.avatarTheme!.constraints, - borderRadius: widget.messageTheme.avatarTheme!.borderRadius, - showOnlineStatus: false, - ), + Widget _buildUserAvatar() => Transform.translate( + offset: Offset( + 0, + widget.translateUserAvatar + ? (widget.messageTheme.avatarTheme?.constraints.maxHeight ?? 40) / + 2 + : 0, + ), + child: UserAvatar( + user: widget.message.user!, + onTap: widget.onUserAvatarTap, + constraints: widget.messageTheme.avatarTheme!.constraints, + borderRadius: widget.messageTheme.avatarTheme!.borderRadius, + showOnlineStatus: false, ), ); Widget _buildTextBubble() { if (widget.message.text!.trim().isEmpty) return const Offstage(); - return Transform( - transform: Matrix4.rotationY(widget.reverse ? pi : 0), - alignment: Alignment.center, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: isOnlyEmoji ? EdgeInsets.zero : widget.textPadding, - child: widget.textBuilder != null - ? widget.textBuilder!(context, widget.message) - : MessageText( - onLinkTap: widget.onLinkTap, - message: widget.message, - onMentionTap: widget.onMentionTap, - messageTheme: isOnlyEmoji - ? widget.messageTheme.copyWith( - messageText: - widget.messageTheme.messageText!.copyWith( - fontSize: 42, - )) - : widget.messageTheme, - ), - ), - if (hasUrlAttachments && !hasQuotedMessage) _buildUrlAttachment(), - ], - ), + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: isOnlyEmoji ? EdgeInsets.zero : widget.textPadding, + child: widget.textBuilder != null + ? widget.textBuilder!(context, widget.message) + : MessageText( + onLinkTap: widget.onLinkTap, + message: widget.message, + onMentionTap: widget.onMentionTap, + messageTheme: isOnlyEmoji + ? widget.messageTheme.copyWith( + messageText: + widget.messageTheme.messageText!.copyWith( + fontSize: 42, + )) + : widget.messageTheme, + ), + ), + if (hasUrlAttachments && !hasQuotedMessage) _buildUrlAttachment(), + ], ); } @@ -1172,10 +1154,15 @@ class _MessageWidgetState extends State } class _ThreadReplyPainter extends CustomPainter { - const _ThreadReplyPainter({this.context, required this.color}); + const _ThreadReplyPainter({ + this.context, + required this.color, + this.reverse = false, + }); final Color? color; final BuildContext? context; + final bool reverse; @override void paint(Canvas canvas, Size size) { @@ -1186,12 +1173,13 @@ class _ThreadReplyPainter extends CustomPainter { ..strokeCap = StrokeCap.round; final path = Path() - ..moveTo(0, 0) - ..quadraticBezierTo(0, size.height * 0.38, 0, size.height * 0.50) + ..moveTo(reverse ? size.width : 0, 0) + ..quadraticBezierTo(reverse ? size.width : 0, size.height * 0.38, + reverse ? size.width : 0, size.height * 0.50) ..quadraticBezierTo( - 0, + reverse ? size.width : 0, size.height, - size.width, + reverse ? 0 : size.width, size.height, ); canvas.drawPath(path, paint); diff --git a/packages/stream_chat_flutter/lib/src/quoted_message_widget.dart b/packages/stream_chat_flutter/lib/src/quoted_message_widget.dart index 6be7ded7..a7c7c798 100644 --- a/packages/stream_chat_flutter/lib/src/quoted_message_widget.dart +++ b/packages/stream_chat_flutter/lib/src/quoted_message_widget.dart @@ -1,5 +1,3 @@ -import 'dart:math'; - import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; @@ -133,21 +131,17 @@ class QuotedMessageWidget extends StatelessWidget { if (_hasAttachments) _parseAttachments(context), if (msg.text!.isNotEmpty) Flexible( - child: Transform( - transform: Matrix4.rotationY(reverse ? pi : 0), - alignment: Alignment.center, - child: MessageText( - message: msg, - messageTheme: isOnlyEmoji && _containsText - ? messageTheme.copyWith( - messageText: messageTheme.messageText?.copyWith( - fontSize: 32, - )) - : messageTheme.copyWith( - messageText: messageTheme.messageText?.copyWith( - fontSize: 12, - )), - ), + child: MessageText( + message: msg, + messageTheme: isOnlyEmoji && _containsText + ? messageTheme.copyWith( + messageText: messageTheme.messageText?.copyWith( + fontSize: 32, + )) + : messageTheme.copyWith( + messageText: messageTheme.messageText?.copyWith( + fontSize: 12, + )), ), ), ].insertBetween(const SizedBox(width: 8)); @@ -218,15 +212,11 @@ class QuotedMessageWidget extends StatelessWidget { } } child = AbsorbPointer(child: child); - return Transform( - transform: Matrix4.rotationY(reverse ? pi : 0), - alignment: Alignment.center, - child: Material( - clipBehavior: Clip.antiAlias, - type: MaterialType.transparency, - shape: attachment.type == 'file' ? null : _getDefaultShape(context), - child: child, - ), + return Material( + clipBehavior: Clip.antiAlias, + type: MaterialType.transparency, + shape: attachment.type == 'file' ? null : _getDefaultShape(context), + child: child, ); } @@ -235,17 +225,13 @@ class QuotedMessageWidget extends StatelessWidget { borderRadius: BorderRadius.circular(8), ); - Widget _buildUserAvatar() => Transform( - transform: Matrix4.rotationY(reverse ? pi : 0), - alignment: Alignment.center, - child: UserAvatar( - user: message.user!, - constraints: const BoxConstraints.tightFor( - height: 24, - width: 24, - ), - showOnlineStatus: false, + Widget _buildUserAvatar() => UserAvatar( + user: message.user!, + constraints: const BoxConstraints.tightFor( + height: 24, + width: 24, ), + showOnlineStatus: false, ); Map diff --git a/packages/stream_chat_flutter/lib/src/reaction_bubble.dart b/packages/stream_chat_flutter/lib/src/reaction_bubble.dart index 82ebc6a9..ce75445b 100644 --- a/packages/stream_chat_flutter/lib/src/reaction_bubble.dart +++ b/packages/stream_chat_flutter/lib/src/reaction_bubble.dart @@ -51,69 +51,66 @@ class ReactionBubble extends StatelessWidget { Widget build(BuildContext context) { final reactionIcons = StreamChatTheme.of(context).reactionIcons; final totalReactions = reactions.length; - final offset = totalReactions > 1 ? 16.0 : 2.0; - return Transform( - transform: Matrix4.rotationY(reverse ? pi : 0), + final offset = + totalReactions > 1 ? 16.0.mirrorConditionally(flipTail) : 2.0; + return Stack( alignment: Alignment.center, - child: Stack( - alignment: Alignment.center, - children: [ - Transform.translate( - offset: Offset(reverse ? offset : -offset, 0), + children: [ + Transform.translate( + offset: Offset(-offset, 0), + child: Container( + padding: const EdgeInsets.all(2), + decoration: BoxDecoration( + color: maskColor, + borderRadius: const BorderRadius.all(Radius.circular(16)), + ), child: Container( - padding: const EdgeInsets.all(2), - decoration: BoxDecoration( - color: maskColor, - borderRadius: const BorderRadius.all(Radius.circular(16)), + padding: EdgeInsets.symmetric( + vertical: 4, + horizontal: totalReactions > 1 ? 4.0 : 0, ), - child: Container( - padding: EdgeInsets.symmetric( - vertical: 4, - horizontal: totalReactions > 1 ? 4 : 0, + decoration: BoxDecoration( + border: Border.all( + color: borderColor, ), - decoration: BoxDecoration( - border: Border.all( - color: borderColor, - ), - color: backgroundColor, - borderRadius: const BorderRadius.all(Radius.circular(14)), - ), - child: LayoutBuilder( - builder: (context, constraints) => Flex( - direction: Axis.horizontal, - mainAxisSize: MainAxisSize.min, - children: [ - if (constraints.maxWidth < double.infinity) - ...reactions - .take((constraints.maxWidth) ~/ 24) - .map((reaction) => _buildReaction( - reactionIcons, - reaction, - context, - )) - .toList(), - if (constraints.maxWidth == double.infinity) - ...reactions - .map((reaction) => _buildReaction( - reactionIcons, - reaction, - context, - )) - .toList(), - ], - ), + color: backgroundColor, + borderRadius: const BorderRadius.all(Radius.circular(14)), + ), + child: LayoutBuilder( + builder: (context, constraints) => Flex( + direction: Axis.horizontal, + mainAxisSize: MainAxisSize.min, + children: [ + if (constraints.maxWidth < double.infinity) + ...reactions + .take((constraints.maxWidth) ~/ 24) + .map((reaction) => _buildReaction( + reactionIcons, + reaction, + context, + )) + .toList(), + if (constraints.maxWidth == double.infinity) + ...reactions + .map((reaction) => _buildReaction( + reactionIcons, + reaction, + context, + )) + .toList(), + ], ), ), ), ), - Positioned( - bottom: 2, - left: reverse ? null : 13, - right: !reverse ? null : 13, - child: _buildReactionsTail(context), - ), - ], - ), + ), + Positioned( + bottom: 2, + left: reverse ? null : 13, + right: reverse ? 13 : null, + child: _buildReactionsTail(context), + ), + ], ); } @@ -158,13 +155,11 @@ class ReactionBubble extends StatelessWidget { borderColor, maskColor, tailCirclesSpace: tailCirclesSpacing, + flipTail: !flipTail, + numberOfReactions: reactions.length, ), ); - return Transform( - transform: Matrix4.rotationY(flipTail ? 0 : pi), - alignment: Alignment.center, - child: tail, - ); + return tail; } } @@ -176,6 +171,8 @@ class ReactionBubblePainter extends CustomPainter { this.borderColor, this.maskColor, { this.tailCirclesSpace = 0, + this.flipTail = false, + this.numberOfReactions = 0, }); /// Color of bubble @@ -190,6 +187,12 @@ class ReactionBubblePainter extends CustomPainter { /// Tail circle space final double tailCirclesSpace; + /// Flip tail + final bool flipTail; + + /// Number of reactions on the page + final int numberOfReactions; + @override void paint(Canvas canvas, Size size) { _drawOvalMask(size, canvas); @@ -213,8 +216,9 @@ class ReactionBubblePainter extends CustomPainter { final path = Path() ..addOval( Rect.fromCircle( - center: - const Offset(4, 3) + Offset(tailCirclesSpace, tailCirclesSpace), + center: const Offset(4, 3).mirrorConditionally(flipTail) + + Offset(tailCirclesSpace, tailCirclesSpace) + .mirrorConditionally(flipTail), radius: 4, ), ); @@ -230,8 +234,9 @@ class ReactionBubblePainter extends CustomPainter { final path = Path() ..addOval( Rect.fromCircle( - center: - const Offset(4, 3) + Offset(tailCirclesSpace, tailCirclesSpace), + center: const Offset(4, 3).mirrorConditionally(flipTail) + + Offset(tailCirclesSpace, tailCirclesSpace) + .mirrorConditionally(flipTail), radius: 2, ), ); @@ -245,7 +250,9 @@ class ReactionBubblePainter extends CustomPainter { final path = Path() ..addOval(Rect.fromCircle( - center: const Offset(4, 3) + Offset(tailCirclesSpace, tailCirclesSpace), + center: const Offset(4, 3).mirrorConditionally(flipTail) + + Offset(tailCirclesSpace, tailCirclesSpace) + .mirrorConditionally(flipTail), radius: 2, )); canvas.drawPath(path, paint); @@ -258,12 +265,12 @@ class ReactionBubblePainter extends CustomPainter { ..style = PaintingStyle.stroke; const dy = -2.2; - const startAngle = 1.1; - const sweepAngle = 1.2; + final startAngle = flipTail ? -0.1 : 1.1; + final sweepAngle = flipTail ? -1.2 : (numberOfReactions > 1 ? 1.2 : 0.9); final path = Path() ..addArc( Rect.fromCircle( - center: const Offset(1, dy), + center: const Offset(1, dy).mirrorConditionally(flipTail), radius: 4, ), -pi * startAngle, @@ -278,12 +285,12 @@ class ReactionBubblePainter extends CustomPainter { ..strokeWidth = 1; const dy = -2.2; - const startAngle = 1; - const sweepAngle = 1.3; + final startAngle = flipTail ? -0.0 : 1.0; + final sweepAngle = flipTail ? -1.3 : 1.3; final path = Path() ..addArc( Rect.fromCircle( - center: const Offset(1, dy), + center: const Offset(1, dy).mirrorConditionally(flipTail), radius: 4, ), -pi * startAngle, @@ -299,12 +306,12 @@ class ReactionBubblePainter extends CustomPainter { ..style = PaintingStyle.fill; const dy = -2.2; - const startAngle = 1.1; - const sweepAngle = 1.2; + final startAngle = flipTail ? -0.1 : 1.1; + final sweepAngle = flipTail ? -1.2 : 1.2; final path = Path() ..addArc( Rect.fromCircle( - center: const Offset(1, dy), + center: const Offset(1, dy).mirrorConditionally(flipTail), radius: 6, ), -pi * startAngle, @@ -316,3 +323,17 @@ class ReactionBubblePainter extends CustomPainter { @override bool shouldRepaint(CustomPainter oldDelegate) => true; } + +/// Extension on [Offset] +extension YTransformer on Offset { + /// Flips x coordinate when flip is true + // ignore: avoid_positional_boolean_parameters + Offset mirrorConditionally(bool flip) => Offset(flip ? -dx : dx, dy); +} + +/// Extension on [Offset] +extension IntTransformer on double { + /// Flips x coordinate when flip is true + // ignore: avoid_positional_boolean_parameters + double mirrorConditionally(bool flip) => flip ? -this : this; +} diff --git a/packages/stream_chat_flutter/lib/src/utils.dart b/packages/stream_chat_flutter/lib/src/utils.dart index ede3e2bd..5a86d4ef 100644 --- a/packages/stream_chat_flutter/lib/src/utils.dart +++ b/packages/stream_chat_flutter/lib/src/utils.dart @@ -1,5 +1,3 @@ -import 'dart:math'; - import 'package:flutter/material.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; @@ -332,18 +330,10 @@ Widget wrapAttachmentWidget( ShapeBorder attachmentShape, // ignore: avoid_positional_boolean_parameters bool reverse, - BorderRadius borderRadius, ) => - ClipRRect( - borderRadius: borderRadius, - child: Material( - clipBehavior: Clip.antiAlias, - shape: attachmentShape, - type: MaterialType.transparency, - child: Transform( - transform: Matrix4.rotationY(reverse ? pi : 0), - alignment: Alignment.center, - child: attachmentWidget, - ), - ), + Material( + clipBehavior: Clip.antiAlias, + shape: attachmentShape, + type: MaterialType.transparency, + child: attachmentWidget, ); diff --git a/packages/stream_chat_flutter/test/src/goldens/reaction_bubble_2.png b/packages/stream_chat_flutter/test/src/goldens/reaction_bubble_2.png index ea03e3e6..05cfdbbe 100644 Binary files a/packages/stream_chat_flutter/test/src/goldens/reaction_bubble_2.png and b/packages/stream_chat_flutter/test/src/goldens/reaction_bubble_2.png differ