diff --git a/packages/flutter_widgets/example/android/app/build.gradle b/packages/flutter_widgets/example/android/app/build.gradle index c5ff1b9e..c90beca2 100644 --- a/packages/flutter_widgets/example/android/app/build.gradle +++ b/packages/flutter_widgets/example/android/app/build.gradle @@ -27,7 +27,7 @@ apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" android { compileSdkVersion 29 - ndkVersion '21.3.6528147' + ndkVersion '22.0.7026061' sourceSets { main.java.srcDirs += 'src/main/kotlin' diff --git a/packages/flutter_widgets/example/lib/main.dart b/packages/flutter_widgets/example/lib/main.dart index 49ee3fdc..7b2f578a 100644 --- a/packages/flutter_widgets/example/lib/main.dart +++ b/packages/flutter_widgets/example/lib/main.dart @@ -763,7 +763,7 @@ class _ChannelPageState extends State { } } -class ThreadPage extends StatelessWidget { +class ThreadPage extends StatefulWidget { final Message parent; final int initialScrollIndex; final double initialAlignment; @@ -775,25 +775,54 @@ class ThreadPage extends StatelessWidget { this.initialAlignment, }) : super(key: key); + @override + _ThreadPageState createState() => _ThreadPageState(); +} + +class _ThreadPageState extends State { + Message _quotedMessage; + FocusNode _focusNode = FocusNode(); + + @override + void dispose() { + _focusNode.dispose(); + super.dispose(); + } + + void _reply(Message message) { + setState(() => _quotedMessage = message); + WidgetsBinding.instance.addPostFrameCallback((timeStamp) { + _focusNode.requestFocus(); + }); + } + @override Widget build(BuildContext context) { return Scaffold( backgroundColor: StreamChatTheme.of(context).colorTheme.whiteSnow, appBar: ThreadHeader( - parent: parent, + parent: widget.parent, ), body: Column( children: [ Expanded( child: MessageListView( - parentMessage: parent, - initialScrollIndex: initialScrollIndex, - initialAlignment: initialAlignment, + parentMessage: widget.parent, + initialScrollIndex: widget.initialScrollIndex, + initialAlignment: widget.initialAlignment, + onMessageSwiped: _reply, + onReplyTap: _reply, ), ), - if (parent.type != 'deleted') + if (widget.parent.type != 'deleted') MessageInput( - parentMessage: parent, + parentMessage: widget.parent, + focusNode: _focusNode, + quotedMessage: _quotedMessage, + onQuotedMessageCleared: () { + setState(() => _quotedMessage = null); + _focusNode.unfocus(); + }, ), ], ), diff --git a/packages/flutter_widgets/example/pubspec.yaml b/packages/flutter_widgets/example/pubspec.yaml index 16699e84..f4197d99 100644 --- a/packages/flutter_widgets/example/pubspec.yaml +++ b/packages/flutter_widgets/example/pubspec.yaml @@ -1,6 +1,6 @@ name: example description: A new Flutter project. -version: 1.1.1+2 +version: 1.1.1+3 environment: sdk: ">=2.2.2 <3.0.0" diff --git a/packages/flutter_widgets/lib/src/channel_bottom_sheet.dart b/packages/flutter_widgets/lib/src/channel_bottom_sheet.dart index d03f233c..6b3e4658 100644 --- a/packages/flutter_widgets/lib/src/channel_bottom_sheet.dart +++ b/packages/flutter_widgets/lib/src/channel_bottom_sheet.dart @@ -74,7 +74,7 @@ class _ChannelBottomSheetState extends State { ), borderRadius: BorderRadius.circular(32.0), onlineIndicatorConstraints: - BoxConstraints.tight(Size(16.0, 16.0)), + BoxConstraints.tight(Size(12.0, 12.0)), ), SizedBox( height: 6.0, @@ -110,6 +110,8 @@ class _ChannelBottomSheetState extends State { maxWidth: 64.0, ), borderRadius: BorderRadius.circular(32.0), + onlineIndicatorConstraints: + BoxConstraints.tight(Size(12.0, 12.0)), ), SizedBox( height: 6.0, diff --git a/packages/flutter_widgets/lib/src/channel_file_display_screen.dart b/packages/flutter_widgets/lib/src/channel_file_display_screen.dart index 6feac224..c6132609 100644 --- a/packages/flutter_widgets/lib/src/channel_file_display_screen.dart +++ b/packages/flutter_widgets/lib/src/channel_file_display_screen.dart @@ -142,7 +142,7 @@ class _ChannelFileDisplayScreenState extends State { } return LazyLoadScrollView( - onEndOfPage: () => messageSearchBloc.search( + onEndOfPage: () => messageSearchBloc.loadMore( filter: { 'cid': { r'$in': ['messaging:${StreamChannel.of(context).channel.id}'] diff --git a/packages/flutter_widgets/lib/src/channel_media_display_screen.dart b/packages/flutter_widgets/lib/src/channel_media_display_screen.dart index ebc95516..8b69fac8 100644 --- a/packages/flutter_widgets/lib/src/channel_media_display_screen.dart +++ b/packages/flutter_widgets/lib/src/channel_media_display_screen.dart @@ -156,7 +156,7 @@ class _ChannelMediaDisplayScreenState extends State { } return LazyLoadScrollView( - onEndOfPage: () => messageSearchBloc.search( + onEndOfPage: () => messageSearchBloc.loadMore( filter: { 'cid': { r'$in': ['messaging:${StreamChannel.of(context).channel.id}'] diff --git a/packages/flutter_widgets/lib/src/file_attachment.dart b/packages/flutter_widgets/lib/src/file_attachment.dart index ca51bf76..96061cab 100644 --- a/packages/flutter_widgets/lib/src/file_attachment.dart +++ b/packages/flutter_widgets/lib/src/file_attachment.dart @@ -62,16 +62,9 @@ class _FileAttachmentState extends State { child: Container( width: widget.size?.width ?? 100, height: 56.0, - decoration: BoxDecoration( - color: StreamChatTheme.of(context).colorTheme.white, - borderRadius: - widget.trailing != null ? BorderRadius.circular(16.0) : null, - border: widget.trailing != null - ? Border.fromBorderSide(BorderSide( - color: StreamChatTheme.of(context).colorTheme.greyWhisper)) - : null, - ), + color: StreamChatTheme.of(context).colorTheme.white, child: Row( + crossAxisAlignment: CrossAxisAlignment.start, children: [ Container( child: _getFileTypeImage(), @@ -79,9 +72,7 @@ class _FileAttachmentState extends State { width: 33.33, margin: EdgeInsets.all(8.0), ), - SizedBox( - width: 6.0, - ), + SizedBox(width: 8.0), Expanded( child: Column( mainAxisAlignment: MainAxisAlignment.center, @@ -93,32 +84,36 @@ class _FileAttachmentState extends State { maxLines: 1, overflow: TextOverflow.ellipsis, ), - SizedBox( - height: 3.0, - ), + SizedBox(height: 3.0), Text( '${getSizeText(widget.attachment.extraData['file_size'])}', - style: StreamChatTheme.of(context).textTheme.body.copyWith( - color: StreamChatTheme.of(context) - .colorTheme - .black - .withOpacity(0.5)), + style: StreamChatTheme.of(context) + .textTheme + .footnote + .copyWith( + color: StreamChatTheme.of(context) + .colorTheme + .black + .withOpacity(0.5)), ), ], ), ), - Column( - children: [ - widget.trailing ?? - IconButton( - icon: StreamSvgIcon.cloud_download( - color: StreamChatTheme.of(context).colorTheme.black, - ), - onPressed: () { - launchURL(context, widget.attachment.assetUrl); - }, + SizedBox(width: 8.0), + Material( + type: MaterialType.transparency, + child: widget.trailing ?? + IconButton( + icon: StreamSvgIcon.cloud_download( + color: StreamChatTheme.of(context).colorTheme.black, ), - ], + padding: const EdgeInsets.all(8), + visualDensity: VisualDensity.compact, + splashRadius: 16, + onPressed: () { + launchURL(context, widget.attachment.assetUrl); + }, + ), ), ], ), diff --git a/packages/flutter_widgets/lib/src/image_actions_modal.dart b/packages/flutter_widgets/lib/src/image_actions_modal.dart index 332117d4..97729ca1 100644 --- a/packages/flutter_widgets/lib/src/image_actions_modal.dart +++ b/packages/flutter_widgets/lib/src/image_actions_modal.dart @@ -8,6 +8,7 @@ import 'package:image_gallery_saver/image_gallery_saver.dart'; import 'package:path_provider/path_provider.dart'; import '../stream_chat_flutter.dart'; +import 'extension.dart'; class ImageActionsModal extends StatelessWidget { final Message message; @@ -35,127 +36,124 @@ class ImageActionsModal extends StatelessWidget { } Widget _buildPage(context) { - return Material( - color: Colors.transparent, - child: Column( - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - SizedBox( - height: kToolbarHeight, - child: IconButton( - icon: StreamSvgIcon.close(), - onPressed: () => Navigator.maybePop(context), + return Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + SizedBox(height: kToolbarHeight), + Padding( + padding: const EdgeInsets.only(right: 8.0), + child: Container( + width: MediaQuery.of(context).size.width * 0.5, + clipBehavior: Clip.hardEdge, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(16.0), ), - ), - Align( - alignment: Alignment.centerRight, child: Container( - width: MediaQuery.of(context).size.width / 1.8, - child: Padding( - padding: const EdgeInsets.all(8.0), - child: Material( - clipBehavior: Clip.hardEdge, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.end, + mainAxisSize: MainAxisSize.min, + children: [ + _buildButton( + context, + 'Reply', + StreamSvgIcon.Icon_curve_line_left_up( + size: 24.0, + color: StreamChatTheme.of(context).colorTheme.grey, + ), + () {}, ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: ListTile.divideTiles( - color: StreamChatTheme.of(context).colorTheme.greyWhisper, - context: context, - tiles: [ - _buildButton( - context, - 'Reply', - StreamSvgIcon.Icon_curve_line_left_up( - size: 24.0, - color: - StreamChatTheme.of(context).colorTheme.grey, - ), - () {}), - _buildButton( - context, - 'Show in Chat', - StreamSvgIcon.eye( - size: 24.0, - color: - StreamChatTheme.of(context).colorTheme.black, - ), - onShowMessage), - _buildButton( - context, - 'Save ${urls[currentIndex].type == 'video' ? 'Video' : 'Image'}', - StreamSvgIcon.Icon_save( - size: 24.0, - color: - StreamChatTheme.of(context).colorTheme.grey, - ), () async { - var url = urls[currentIndex].imageUrl ?? - urls[currentIndex].assetUrl ?? - urls[currentIndex].thumbUrl; + _buildButton( + context, + 'Show in Chat', + StreamSvgIcon.eye( + size: 24.0, + color: StreamChatTheme.of(context).colorTheme.black, + ), + onShowMessage, + ), + _buildButton( + context, + 'Save ${urls[currentIndex].type == 'video' ? 'Video' : 'Image'}', + StreamSvgIcon.Icon_save( + size: 24.0, + color: StreamChatTheme.of(context).colorTheme.grey, + ), + () async { + var url = urls[currentIndex].imageUrl ?? + urls[currentIndex].assetUrl ?? + urls[currentIndex].thumbUrl; - if (urls[currentIndex].type == 'video') { - await _saveVideo(url); - Navigator.pop(context); - } else { - await _saveImage(url); - Navigator.pop(context); - } - }), - if (StreamChat.of(context).user.id == message.user.id) - _buildButton( - context, - 'Delete', - StreamSvgIcon.delete( - size: 24.0, - color: StreamChatTheme.of(context) - .colorTheme - .accentRed, - ), - () { - Navigator.pop(context); - Navigator.pop(context); - StreamChat.of(context).client.deleteMessage( - message, - StreamChannel.of(context).channel.cid, - ); - }, - color: StreamChatTheme.of(context) - .colorTheme - .accentRed, - ), - ], - ).toList(), + if (urls[currentIndex].type == 'video') { + await _saveVideo(url); + Navigator.pop(context); + } else { + await _saveImage(url); + Navigator.pop(context); + } + }, ), - ), + if (StreamChat.of(context).user.id == message.user.id) + _buildButton( + context, + 'Delete', + StreamSvgIcon.delete( + size: 24.0, + color: StreamChatTheme.of(context).colorTheme.accentRed, + ), + () { + Navigator.pop(context); + Navigator.pop(context); + StreamChat.of(context).client.deleteMessage( + message, + StreamChannel.of(context).channel.cid, + ); + }, + color: StreamChatTheme.of(context).colorTheme.accentRed, + ), + ] + .map((e) => + Align(alignment: Alignment.centerRight, child: e)) + .insertBetween( + Container( + height: 1, + color: + StreamChatTheme.of(context).colorTheme.greyWhisper, + ), + ), ), ), ), - ], - ), + ) + ], ); } Widget _buildButton( - context, String title, StreamSvgIcon icon, VoidCallback onTap, - {Color color}) { - var titleStyle = TextStyle( - fontSize: 14.5, - color: StreamChatTheme.of(context).colorTheme.black, - ); - + context, + String title, + StreamSvgIcon icon, + VoidCallback onTap, { + Color color, + }) { return Material( color: StreamChatTheme.of(context).colorTheme.white, child: InkWell( onTap: onTap, - child: ListTile( - dense: true, - title: Text( - title, - style: - color == null ? titleStyle : titleStyle.copyWith(color: color), + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 16.0), + child: Row( + children: [ + icon, + SizedBox(width: 16), + Text( + title, + style: StreamChatTheme.of(context) + .textTheme + .body + .copyWith(color: color), + ), + ], ), - leading: icon, ), ), ); diff --git a/packages/flutter_widgets/lib/src/image_footer.dart b/packages/flutter_widgets/lib/src/image_footer.dart index e43eb3fb..9daf9a5c 100644 --- a/packages/flutter_widgets/lib/src/image_footer.dart +++ b/packages/flutter_widgets/lib/src/image_footer.dart @@ -218,12 +218,14 @@ class _ImageFooterState extends State { shrinkWrap: true, physics: NeverScrollableScrollPhysics(), itemBuilder: (context, position) { + Widget media; + if (widget.mediaAttachments[position].type == 'video') { var controllerPackage = widget.videoPackages[ videoAttachments .indexOf(widget.mediaAttachments[position])]; - return InkWell( + media = InkWell( onTap: () { widget.mediaSelectedCallBack(position); }, @@ -235,7 +237,7 @@ class _ImageFooterState extends State { ), ); } else { - return InkWell( + media = InkWell( onTap: () { widget.mediaSelectedCallBack(position); }, @@ -254,6 +256,28 @@ class _ImageFooterState extends State { ), ); } + + return Stack( + children: [ + media, + Padding( + padding: EdgeInsets.all(8.0), + child: Align( + alignment: Alignment.topLeft, + child: UserAvatar( + user: widget.message.user, + constraints: BoxConstraints.tight( + Size( + 24, + 24, + ), + ), + showOnlineStatus: false, + ), + ), + ), + ], + ); }, itemCount: widget.mediaAttachments.length, gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( diff --git a/packages/flutter_widgets/lib/src/message_actions_modal.dart b/packages/flutter_widgets/lib/src/message_actions_modal.dart index 2d125bab..9b93695e 100644 --- a/packages/flutter_widgets/lib/src/message_actions_modal.dart +++ b/packages/flutter_widgets/lib/src/message_actions_modal.dart @@ -14,6 +14,7 @@ import 'message_input.dart'; import 'message_widget.dart'; import 'stream_chat.dart'; import 'stream_chat_theme.dart'; +import 'extension.dart'; class MessageActionsModal extends StatefulWidget { final Widget Function(BuildContext, Message) editMessageInputBuilder; @@ -89,6 +90,8 @@ class _MessageActionsModalState extends State { duration: Duration(milliseconds: 300), curve: Curves.easeInOutBack, builder: (context, val, snapshot) { + final hasFileAttachment = + widget.message.attachments?.any((it) => it.type == 'file') == true; return GestureDetector( behavior: HitTestBehavior.translucent, onTap: () => Navigator.maybePop(context), @@ -110,7 +113,7 @@ class _MessageActionsModalState extends State { child: Center( child: SingleChildScrollView( child: Padding( - padding: const EdgeInsets.symmetric(vertical: 8.0), + padding: const EdgeInsets.all(8.0), child: Column( crossAxisAlignment: widget.reverse ? CrossAxisAlignment.end @@ -135,6 +138,7 @@ class _MessageActionsModalState extends State { messageTheme: widget.messageTheme, ), ), + SizedBox(height: 8), IgnorePointer( child: MessageWidget( key: Key('MessageWidget'), @@ -150,8 +154,17 @@ class _MessageActionsModalState extends State { showThreadReplyIndicator: false, showReplyMessage: false, showUserAvatar: widget.showUserAvatar, + attachmentPadding: EdgeInsets.all( + hasFileAttachment ? 4 : 2, + ), showTimestamp: false, translateUserAvatar: false, + padding: const EdgeInsets.all(0), + textPadding: EdgeInsets.symmetric( + vertical: 8.0, + horizontal: + widget.message.text.isOnlyEmoji ? 0 : 16.0, + ), showReactionPickerIndicator: widget.showReactions && (widget.message.status == @@ -162,10 +175,10 @@ class _MessageActionsModalState extends State { shape: widget.messageShape, ), ), + SizedBox(height: 8), Padding( padding: EdgeInsets.only( - right: widget.reverse ? 8 : 0, - left: widget.reverse ? 0 : 48, + left: widget.reverse ? 0 : 40, ), child: SizedBox( width: MediaQuery.of(context).size.width * 0.75, @@ -180,36 +193,37 @@ class _MessageActionsModalState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, - children: ListTile.divideTiles( - color: StreamChatTheme.of(context) - .colorTheme - .greyWhisper, - context: context, - tiles: [ - if (widget.showReplyMessage && - (widget.message.status == - MessageSendingStatus.SENT || - widget.message.status == null) && - widget.message.parentId == null) - _buildReplyButton(context), - if (widget.showThreadReplyMessage && - (widget.message.status == - MessageSendingStatus.SENT || - widget.message.status == null) && - widget.message.parentId == null) - _buildThreadReplyButton(context), - if (widget.showResendMessage) - _buildResendMessage(context), - if (widget.showEditMessage) - _buildEditMessage(context), - if (widget.showCopyMessage) - _buildCopyButton(context), - if (widget.showFlagButton) - _buildFlagButton(context), - if (widget.showDeleteMessage) - _buildDeleteButton(context), - ], - ).toList(), + children: [ + if (widget.showReplyMessage && + (widget.message.status == + MessageSendingStatus.SENT || + widget.message.status == null) && + widget.message.parentId == null) + _buildReplyButton(context), + if (widget.showThreadReplyMessage && + (widget.message.status == + MessageSendingStatus.SENT || + widget.message.status == null) && + widget.message.parentId == null) + _buildThreadReplyButton(context), + if (widget.showResendMessage) + _buildResendMessage(context), + if (widget.showEditMessage) + _buildEditMessage(context), + if (widget.showCopyMessage) + _buildCopyButton(context), + if (widget.showFlagButton) + _buildFlagButton(context), + if (widget.showDeleteMessage) + _buildDeleteButton(context), + ].insertBetween( + Container( + height: 1, + color: StreamChatTheme.of(context) + .colorTheme + .greyWhisper, + ), + ), ), ), ), @@ -410,136 +424,131 @@ class _MessageActionsModalState extends State { } Widget _buildReplyButton(BuildContext context) { - return ListTile( - dense: true, - title: Row( - children: [ - StreamSvgIcon.reply( - color: StreamChatTheme.of(context).primaryIconTheme.color, - ), - const SizedBox(width: 16), - Text( - 'Reply', - style: StreamChatTheme.of(context).textTheme.headline, - ), - ], - ), + return InkWell( onTap: () { Navigator.pop(context); if (widget.onReplyTap != null) { widget.onReplyTap(widget.message); } }, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 16.0), + child: Row( + children: [ + StreamSvgIcon.reply( + color: StreamChatTheme.of(context).primaryIconTheme.color, + ), + const SizedBox(width: 16), + Text( + 'Reply', + style: StreamChatTheme.of(context).textTheme.body, + ), + ], + ), + ), ); } Widget _buildFlagButton(BuildContext context) { - return ListTile( - dense: true, - title: Row( - children: [ - StreamSvgIcon.icon_flag( - color: StreamChatTheme.of(context).primaryIconTheme.color, - ), - const SizedBox(width: 16), - Text( - 'Flag', - style: StreamChatTheme.of(context).textTheme.headline, - ), - ], - ), + return InkWell( onTap: () => _showFlagDialog(), + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 16.0), + child: Row( + children: [ + StreamSvgIcon.icon_flag( + color: StreamChatTheme.of(context).primaryIconTheme.color, + ), + const SizedBox(width: 16), + Text( + 'Flag', + style: StreamChatTheme.of(context).textTheme.body, + ), + ], + ), + ), ); } Widget _buildDeleteButton(BuildContext context) { final isDeleteFailed = widget.message.status == MessageSendingStatus.FAILED_DELETE; - return ListTile( - dense: true, - title: Row( - children: [ - StreamSvgIcon.delete( - color: Colors.red, - ), - const SizedBox(width: 16), - Text( - isDeleteFailed ? 'Retry Deleting Message' : 'Delete Message', - style: StreamChatTheme.of(context) - .textTheme - .headline - .copyWith(color: Colors.red), - ), - ], + return InkWell( + onTap: () => _showDeleteDialog(), + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 16.0), + child: Row( + children: [ + StreamSvgIcon.delete( + color: Colors.red, + ), + const SizedBox(width: 16), + Text( + isDeleteFailed ? 'Retry Deleting Message' : 'Delete Message', + style: StreamChatTheme.of(context) + .textTheme + .body + .copyWith(color: Colors.red), + ), + ], + ), ), - onTap: () { - _showDeleteDialog(); - }, ); } Widget _buildCopyButton(BuildContext context) { - return ListTile( - dense: true, - title: Row( - children: [ - StreamSvgIcon.copy( - size: 24, - color: StreamChatTheme.of(context).primaryIconTheme.color, - ), - const SizedBox(width: 16), - Text( - 'Copy Message', - style: StreamChatTheme.of(context).textTheme.headline, - ), - ], - ), + return InkWell( onTap: () async { await Clipboard.setData(ClipboardData(text: widget.message.text)); Navigator.pop(context); }, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 16.0), + child: Row( + children: [ + StreamSvgIcon.copy( + size: 24, + color: StreamChatTheme.of(context).primaryIconTheme.color, + ), + const SizedBox(width: 16), + Text( + 'Copy Message', + style: StreamChatTheme.of(context).textTheme.body, + ), + ], + ), + ), ); } Widget _buildEditMessage(BuildContext context) { - return ListTile( - dense: true, - title: Row( - children: [ - StreamSvgIcon.edit( - color: StreamChatTheme.of(context).primaryIconTheme.color, - ), - const SizedBox(width: 16), - Text( - 'Edit Message', - style: StreamChatTheme.of(context).textTheme.headline, - ), - ], - ), + return InkWell( onTap: () async { Navigator.pop(context); _showEditBottomSheet(context); }, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 16.0), + child: Row( + children: [ + StreamSvgIcon.edit( + color: StreamChatTheme.of(context).primaryIconTheme.color, + ), + const SizedBox(width: 16), + Text( + 'Edit Message', + style: StreamChatTheme.of(context).textTheme.body, + ), + ], + ), + ), ); } Widget _buildResendMessage(BuildContext context) { final isUpdateFailed = widget.message.status == MessageSendingStatus.FAILED_UPDATE; - return ListTile( - dense: true, - title: Row( - children: [ - StreamSvgIcon.circle_up( - color: StreamChatTheme.of(context).colorTheme.accentBlue, - ), - const SizedBox(width: 16), - Text( - isUpdateFailed ? 'Resend Edited Message' : 'Resend', - style: StreamChatTheme.of(context).textTheme.headline, - ), - ], - ), + return InkWell( onTap: () { Navigator.pop(context); final client = StreamChat.of(context).client; @@ -550,6 +559,21 @@ class _MessageActionsModalState extends State { channel.sendMessage(widget.message); } }, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 16.0), + child: Row( + children: [ + StreamSvgIcon.circle_up( + color: StreamChatTheme.of(context).colorTheme.accentBlue, + ), + const SizedBox(width: 16), + Text( + isUpdateFailed ? 'Resend Edited Message' : 'Resend', + style: StreamChatTheme.of(context).textTheme.body, + ), + ], + ), + ), ); } @@ -623,26 +647,28 @@ class _MessageActionsModalState extends State { } Widget _buildThreadReplyButton(BuildContext context) { - return ListTile( - dense: true, - title: Row( - children: [ - StreamSvgIcon.thread( - color: StreamChatTheme.of(context).primaryIconTheme.color, - ), - const SizedBox(width: 16), - Text( - 'Thread Reply', - style: StreamChatTheme.of(context).textTheme.headline, - ), - ], - ), + return InkWell( onTap: () { Navigator.pop(context); if (widget.onThreadReplyTap != null) { widget.onThreadReplyTap(widget.message); } }, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 16.0), + child: Row( + children: [ + StreamSvgIcon.thread( + color: StreamChatTheme.of(context).primaryIconTheme.color, + ), + const SizedBox(width: 16), + Text( + 'Thread Reply', + style: StreamChatTheme.of(context).textTheme.body, + ), + ], + ), + ), ); } } diff --git a/packages/flutter_widgets/lib/src/message_input.dart b/packages/flutter_widgets/lib/src/message_input.dart index cc12fbd9..22a36882 100644 --- a/packages/flutter_widgets/lib/src/message_input.dart +++ b/packages/flutter_widgets/lib/src/message_input.dart @@ -1527,14 +1527,12 @@ class MessageInputState extends State { return Transform( transform: Matrix4.rotationY(pi), alignment: Alignment.center, - child: Padding( + child: QuotedMessageWidget( + reverse: true, + showBorder: !containsUrl, + message: widget.quotedMessage, + messageTheme: StreamChatTheme.of(context).otherMessageTheme, padding: const EdgeInsets.fromLTRB(8, 8, 8, 0), - child: QuotedMessageWidget( - reverse: true, - showBorder: !containsUrl, - message: widget.quotedMessage, - messageTheme: StreamChatTheme.of(context).otherMessageTheme, - ), ), ); } @@ -1544,17 +1542,17 @@ class MessageInputState extends State { return Column( children: [ if (_attachments.any((e) => e.attachment?.type == 'file')) - LimitedBox( - maxHeight: 136.0, - child: ListView( - reverse: true, - shrinkWrap: true, - children: _attachments.reversed - .where((e) => e.attachment?.type == 'file') - .map( - (e) => Padding( - padding: const EdgeInsets.symmetric(horizontal: 8.0), - child: ClipRRect( + Padding( + padding: const EdgeInsets.fromLTRB(8, 8, 8, 0), + child: LimitedBox( + maxHeight: 136.0, + child: ListView( + reverse: true, + shrinkWrap: true, + children: _attachments.reversed + .where((e) => e.attachment?.type == 'file') + .map( + (e) => ClipRRect( borderRadius: BorderRadius.circular(10), clipBehavior: Clip.antiAlias, child: FileAttachment( @@ -1580,31 +1578,28 @@ class MessageInputState extends State { .white, ), ), - onTap: () { - setState(() { - _attachments.remove(e); - }); - }, + onTap: () => + setState(() => _attachments.remove(e)), ), ), ), ), - ), - ) - .toList(), + ) + .insertBetween(const SizedBox(width: 8)), + ), ), ), if (_attachments.any((e) => e.attachment?.type != 'file')) - LimitedBox( - maxHeight: 104.0, - child: ListView( - scrollDirection: Axis.horizontal, - children: _attachments - .where((e) => e.attachment?.type != 'file') - .map( - (attachment) => Padding( - padding: const EdgeInsets.all(8.0), - child: ClipRRect( + Padding( + padding: const EdgeInsets.fromLTRB(8, 8, 8, 0), + child: LimitedBox( + maxHeight: 104.0, + child: ListView( + scrollDirection: Axis.horizontal, + children: _attachments + .where((e) => e.attachment?.type != 'file') + .map( + (attachment) => ClipRRect( borderRadius: BorderRadius.circular(10), clipBehavior: Clip.antiAlias, child: Stack( @@ -1631,9 +1626,9 @@ class MessageInputState extends State { ], ), ), - ), - ) - .toList(), + ) + .insertBetween(const SizedBox(width: 8)), + ), ), ), ], diff --git a/packages/flutter_widgets/lib/src/message_list_view.dart b/packages/flutter_widgets/lib/src/message_list_view.dart index f60b9f5e..a0b5a60d 100644 --- a/packages/flutter_widgets/lib/src/message_list_view.dart +++ b/packages/flutter_widgets/lib/src/message_list_view.dart @@ -333,172 +333,167 @@ class _MessageListViewState extends State { childAnchor: Alignment.topCenter, message: statusString, child: LazyLoadScrollView( - child: LazyLoadScrollView( - onStartOfPage: () async { - _inBetweenList = false; - if (!_upToDate) { - _topPaginationActive = false; - _bottomPaginationActive = true; - return _paginateData( + onStartOfPage: () async { + _inBetweenList = false; + if (!_upToDate) { + _topPaginationActive = false; + _bottomPaginationActive = true; + return _paginateData( + streamChannel, + QueryDirection.bottom, + ); + } + }, + onEndOfPage: () async { + _inBetweenList = false; + _topPaginationActive = true; + _bottomPaginationActive = false; + return _paginateData( + streamChannel, + QueryDirection.top, + ); + }, + onInBetweenOfPage: () { + _inBetweenList = true; + }, + child: ScrollablePositionedList.separated( + key: ValueKey(initialIndex + initialAlignment), + itemPositionsListener: _itemPositionListener, + addAutomaticKeepAlives: true, + initialScrollIndex: initialIndex ?? 0, + initialAlignment: initialAlignment ?? 0, + physics: widget.scrollPhysics, + itemScrollController: _scrollController, + reverse: true, + itemCount: messages.length + + 2 + + (_isThreadConversation ? 1 : 0), + separatorBuilder: (context, i) { + if (i == messages.length) return Offstage(); + if (i == 0) return SizedBox(height: 30); + if (i == messages.length + 1) { + final replyCount = + widget.parentMessage.replyCount; + return Container( + decoration: BoxDecoration( + gradient: StreamChatTheme.of(context) + .colorTheme + .bgGradient, + ), + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Text( + '$replyCount ${replyCount == 1 ? 'Reply' : 'Replies'}', + textAlign: TextAlign.center, + style: StreamChatTheme.of(context) + .channelTheme + .channelHeaderTheme + .lastMessageAt, + ), + ), + ); + } + + final message = messages[i]; + final nextMessage = messages[i - 1]; + if (!Jiffy(message.createdAt.toLocal()).isSame( + nextMessage.createdAt.toLocal(), + Units.DAY, + )) { + final divider = widget.dateDividerBuilder != null + ? widget.dateDividerBuilder( + nextMessage.createdAt.toLocal(), + ) + : DateDivider( + dateTime: nextMessage.createdAt.toLocal(), + ); + return Padding( + padding: + const EdgeInsets.symmetric(vertical: 12.0), + child: divider, + ); + } + final timeDiff = + Jiffy(nextMessage.createdAt.toLocal()).diff( + message.createdAt.toLocal(), + Units.MINUTE, + ); + + final isNextUserSame = + message.user.id == nextMessage.user?.id; + final isThread = message.replyCount > 0; + final isDeleted = message.isDeleted; + if (timeDiff >= 1 || + !isNextUserSame || + isThread || + isDeleted) { + return SizedBox(height: 8); + } + return SizedBox(height: 2); + }, + itemBuilder: (context, i) { + if (i == messages.length + 2) { + if (widget.parentMessageBuilder != null) { + return widget.parentMessageBuilder( + context, + widget.parentMessage, + ); + } else { + return buildParentMessage(widget.parentMessage); + } + } + if (i == messages.length + 1) { + return _buildLoadingIndicator( + streamChannel, + QueryDirection.top, + ); + } + if (i == 0) { + return _buildLoadingIndicator( streamChannel, QueryDirection.bottom, ); } - }, - onEndOfPage: () async { - _inBetweenList = false; - _topPaginationActive = true; - _bottomPaginationActive = false; - return _paginateData( - streamChannel, - QueryDirection.top, - ); - }, - onInBetweenOfPage: () { - _inBetweenList = true; - }, - child: ScrollablePositionedList.separated( - key: ValueKey(initialIndex + initialAlignment), - itemPositionsListener: _itemPositionListener, - addAutomaticKeepAlives: true, - initialScrollIndex: initialIndex ?? 0, - initialAlignment: initialAlignment ?? 0, - physics: widget.scrollPhysics, - itemScrollController: _scrollController, - reverse: true, - itemCount: messages.length + - 2 + - (_isThreadConversation ? 1 : 0), - separatorBuilder: (context, i) { - if (i == messages.length) return Offstage(); - if (i == 0) return SizedBox(height: 30); - if (i == messages.length + 1) { - final replyCount = - widget.parentMessage.replyCount; - return Container( - decoration: BoxDecoration( - gradient: StreamChatTheme.of(context) - .colorTheme - .bgGradient, - ), - child: Padding( - padding: const EdgeInsets.all(8.0), - child: Text( - '$replyCount ${replyCount == 1 ? 'Reply' : 'Replies'}', - textAlign: TextAlign.center, - style: StreamChatTheme.of(context) - .channelTheme - .channelHeaderTheme - .lastMessageAt, - ), - ), - ); - } + final message = messages[i - 1]; - final message = messages[i]; - final nextMessage = messages[i - 1]; - if (!Jiffy(message.createdAt.toLocal()).isSame( - nextMessage.createdAt.toLocal(), - Units.DAY, - )) { - final divider = - widget.dateDividerBuilder != null - ? widget.dateDividerBuilder( - nextMessage.createdAt.toLocal(), - ) - : DateDivider( - dateTime: - nextMessage.createdAt.toLocal(), - ); - return Padding( - padding: const EdgeInsets.symmetric( - vertical: 12.0), - child: divider, - ); - } - final timeDiff = - Jiffy(nextMessage.createdAt.toLocal()).diff( - message.createdAt.toLocal(), - Units.MINUTE, + Widget messageWidget; + + if (i == 1) { + messageWidget = _buildBottomMessage( + context, + message, + messages, + streamChannel, ); - - final isNextUserSame = - message.user.id == nextMessage.user?.id; - final isThread = message.replyCount > 0; - final isDeleted = message.isDeleted; - if (timeDiff >= 1 || - !isNextUserSame || - isThread || - isDeleted) { - return SizedBox(height: 8); - } - return SizedBox(height: 2); - }, - itemBuilder: (context, i) { - if (i == messages.length + 2) { - if (widget.parentMessageBuilder != null) { - return widget.parentMessageBuilder( - context, - widget.parentMessage, - ); - } else { - return buildParentMessage( - widget.parentMessage); - } - } - if (i == messages.length + 1) { - return _buildLoadingIndicator( - streamChannel, - QueryDirection.top, - ); - } - if (i == 0) { - return _buildLoadingIndicator( - streamChannel, - QueryDirection.bottom, - ); - } - final message = messages[i - 1]; - - Widget messageWidget; - - if (i == 1) { - messageWidget = _buildBottomMessage( - context, - message, - messages, - streamChannel, - ); - } else if (i == messages.length - 1) { - messageWidget = _buildTopMessage( - 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: (context) => widget.messageBuilder( + context, + MessageDetails( + context, + message, + messages, + i, + ), + messages), ); } else { - if (widget.messageBuilder != null) { - messageWidget = Builder( - key: ValueKey( - 'MESSAGE-${message.id}'), - builder: (context) => widget.messageBuilder( - context, - MessageDetails( - context, - message, - messages, - i, - ), - messages), - ); - } else { - messageWidget = - buildMessage(message, messages, i); - } + messageWidget = + buildMessage(message, messages, i); } - return messageWidget; - }, - ), + } + return messageWidget; + }, ), ), ); @@ -775,6 +770,10 @@ class _MessageListViewState extends State { topRight: Radius.circular(16), bottomRight: Radius.circular(16), ), + textPadding: EdgeInsets.symmetric( + vertical: 8.0, + horizontal: isOnlyEmoji ? 0 : 16.0, + ), borderSide: isMyMessage || isOnlyEmoji ? BorderSide.none : null, showUserAvatar: isMyMessage ? DisplayWidget.gone : DisplayWidget.show, messageTheme: isMyMessage @@ -822,7 +821,7 @@ class _MessageListViewState extends State { final allRead = readList.length >= (channel.memberCount ?? 0) - 1; final hasFileAttachment = - message.attachments.any((it) => it.type == 'file'); + message.attachments?.any((it) => it.type == 'file') == true; final isThreadMessage = message?.parentId != null && message?.showInChannel == true; @@ -854,13 +853,11 @@ class _MessageListViewState extends State { final showThreadReplyIndicator = !_isThreadConversation && hasReplies; final isOnlyEmoji = message.text.isOnlyEmoji; - final showMessageBorder = - showThreadReplyIndicator || showInChannelIndicator; - final borderSide = isMyMessage - ? !showMessageBorder - ? BorderSide.none - : null - : isOnlyEmoji && !showMessageBorder + final hasUrlAttachment = + message.attachments?.any((it) => it.ogScrapeUrl != null) == true; + + final borderSide = + isOnlyEmoji || hasUrlAttachment || (isMyMessage && !hasFileAttachment) ? BorderSide.none : null; @@ -906,14 +903,15 @@ class _MessageListViewState extends State { attachmentBorderRadiusGeometry: BorderRadius.only( topLeft: Radius.circular(attachmentBorderRadius), bottomLeft: Radius.circular( - (timeDiff >= 1 || !isNextUserSame) && !(hasReplies || isThreadMessage) + (timeDiff >= 1 || !isNextUserSame) && + !(hasReplies || isThreadMessage || hasFileAttachment) ? 0 : attachmentBorderRadius, ), topRight: Radius.circular(attachmentBorderRadius), bottomRight: Radius.circular(attachmentBorderRadius), ), - attachmentPadding: const EdgeInsets.all(2), + attachmentPadding: EdgeInsets.all(hasFileAttachment ? 4 : 2), borderRadiusGeometry: BorderRadius.only( topLeft: Radius.circular(16), bottomLeft: Radius.circular( @@ -924,6 +922,10 @@ class _MessageListViewState extends State { topRight: Radius.circular(16), bottomRight: Radius.circular(16), ), + textPadding: EdgeInsets.symmetric( + vertical: 8.0, + horizontal: isOnlyEmoji ? 0 : 16.0, + ), messageTheme: isMyMessage ? StreamChatTheme.of(context).ownMessageTheme : StreamChatTheme.of(context).otherMessageTheme, diff --git a/packages/flutter_widgets/lib/src/message_reactions_modal.dart b/packages/flutter_widgets/lib/src/message_reactions_modal.dart index 15a301cc..c96b3afa 100644 --- a/packages/flutter_widgets/lib/src/message_reactions_modal.dart +++ b/packages/flutter_widgets/lib/src/message_reactions_modal.dart @@ -9,6 +9,7 @@ import 'package:stream_chat_flutter/src/user_avatar.dart'; import 'message_widget.dart'; import 'stream_chat_theme.dart'; +import 'extension.dart'; class MessageReactionsModal extends StatelessWidget { final Widget Function(BuildContext, Message) editMessageInputBuilder; @@ -61,6 +62,8 @@ class MessageReactionsModal extends StatelessWidget { duration: Duration(milliseconds: 300), curve: Curves.easeInOutBack, builder: (context, val, snapshot) { + final hasFileAttachment = + message.attachments?.any((it) => it.type == 'file') == true; return GestureDetector( behavior: HitTestBehavior.translucent, onTap: () => Navigator.maybePop(context), @@ -82,7 +85,7 @@ class MessageReactionsModal extends StatelessWidget { child: Center( child: SingleChildScrollView( child: Padding( - padding: const EdgeInsets.symmetric(vertical: 8.0), + padding: const EdgeInsets.all(8.0), child: Column( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.stretch, @@ -105,6 +108,7 @@ class MessageReactionsModal extends StatelessWidget { messageTheme: messageTheme, ), ), + const SizedBox(height: 8), IgnorePointer( child: MessageWidget( key: Key('MessageWidget'), @@ -123,15 +127,25 @@ class MessageReactionsModal extends StatelessWidget { translateUserAvatar: false, showSendingIndicator: false, shape: messageShape, + padding: const EdgeInsets.all(0), + attachmentPadding: EdgeInsets.all( + hasFileAttachment ? 4 : 2, + ), showInChannelIndicator: false, + textPadding: EdgeInsets.symmetric( + vertical: 8.0, + horizontal: message.text.isOnlyEmoji ? 0 : 16.0, + ), showReactionPickerIndicator: showReactions && (message.status == MessageSendingStatus.SENT || message.status == null), ), ), - if (message.latestReactions?.isNotEmpty == true) + if (message.latestReactions?.isNotEmpty == true) ...[ + const SizedBox(height: 8), _buildReactionCard(context), + ] ], ), ), @@ -145,47 +159,42 @@ class MessageReactionsModal extends StatelessWidget { ); } - Padding _buildReactionCard(BuildContext context) { + Widget _buildReactionCard(BuildContext context) { final currentUser = StreamChat.of(context).user; - return Padding( - padding: const EdgeInsets.symmetric( - horizontal: 8.0, + return Card( + color: StreamChatTheme.of(context).colorTheme.white, + clipBehavior: Clip.hardEdge, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), ), - child: Card( - color: StreamChatTheme.of(context).colorTheme.white, - clipBehavior: Clip.hardEdge, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(16), - ), - margin: EdgeInsets.zero, - child: Padding( - padding: const EdgeInsets.all(16), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Text( - 'Message Reactions', - style: StreamChatTheme.of(context).textTheme.headlineBold, - ), - const SizedBox(height: 16), - Flexible( - child: SingleChildScrollView( - child: Wrap( - spacing: 16, - runSpacing: 16, - alignment: WrapAlignment.start, - children: message.latestReactions - .map((e) => _buildReaction( - e, - currentUser, - context, - )) - .toList(), - ), + margin: EdgeInsets.zero, + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + 'Message Reactions', + style: StreamChatTheme.of(context).textTheme.headlineBold, + ), + const SizedBox(height: 16), + Flexible( + child: SingleChildScrollView( + child: Wrap( + spacing: 16, + runSpacing: 16, + alignment: WrapAlignment.start, + children: message.latestReactions + .map((e) => _buildReaction( + e, + currentUser, + context, + )) + .toList(), ), ), - ], - ), + ), + ], ), ), ); @@ -208,6 +217,7 @@ class MessageReactionsModal extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.center, children: [ Stack( + overflow: Overflow.visible, children: [ UserAvatar( onTap: onUserAvatarTap, @@ -216,6 +226,10 @@ class MessageReactionsModal extends StatelessWidget { height: 64, width: 64, ), + onlineIndicatorConstraints: BoxConstraints.tightFor( + height: 12, + width: 12, + ), borderRadius: BorderRadius.circular(32), ), Positioned( @@ -227,12 +241,14 @@ class MessageReactionsModal extends StatelessWidget { flipTail: !reverse, borderColor: messageTheme.reactionsBorderColor, backgroundColor: messageTheme.reactionsBackgroundColor, + maskColor: StreamChatTheme.of(context).colorTheme.white, + tailCirclesSpacing: 1, highlightOwnReactions: false, ), ), bottom: 6, - left: isCurrentUser ? 0 : null, - right: isCurrentUser ? 0 : null, + left: isCurrentUser ? -3 : null, + right: isCurrentUser ? -3 : null, ), ], ), diff --git a/packages/flutter_widgets/lib/src/message_widget.dart b/packages/flutter_widgets/lib/src/message_widget.dart index ccefce6e..070bdad5 100644 --- a/packages/flutter_widgets/lib/src/message_widget.dart +++ b/packages/flutter_widgets/lib/src/message_widget.dart @@ -278,6 +278,9 @@ class _MessageWidgetState extends State { ?.isNotEmpty == true; + bool get hasUrlAttachments => + widget.message.attachments?.any((it) => it.ogScrapeUrl != null) == true; + bool get showBottomRow => showThreadReplyIndicator || showUsername || @@ -416,7 +419,7 @@ class _MessageWidgetState extends State { if (hasQuotedMessage) _buildQuotedMessage(), if (hasNonUrlAttachments) - ..._parseAttachments( + _parseAttachments( context), if (widget.message.text .trim() @@ -431,7 +434,7 @@ class _MessageWidgetState extends State { ), if (widget.showReactionPickerIndicator) Positioned( - right: 0, + right: 4, top: -8, child: Transform( transform: Matrix4.rotationY( @@ -441,9 +444,9 @@ class _MessageWidgetState extends State { StreamChatTheme.of(context) .colorTheme .white, - StreamChatTheme.of(context) - .colorTheme - .white, + Colors.transparent, + Colors.transparent, + tailCirclesSpace: 1, ), ), ), @@ -494,21 +497,15 @@ class _MessageWidgetState extends State { widget.onQuotedMessageTap != null ? () => widget.onQuotedMessageTap(widget.message.quotedMessageId) : null; - return Padding( + return QuotedMessageWidget( + onTap: onTap, + message: widget.message.quotedMessage, + messageTheme: isMyMessage + ? StreamChatTheme.of(context).otherMessageTheme + : StreamChatTheme.of(context).ownMessageTheme, + reverse: widget.reverse, padding: EdgeInsets.only( - right: 8, - left: 8, - top: 8, - bottom: hasNonUrlAttachments ? 8 : 0, - ), - child: QuotedMessageWidget( - onTap: onTap, - message: widget.message.quotedMessage, - messageTheme: isMyMessage - ? StreamChatTheme.of(context).otherMessageTheme - : StreamChatTheme.of(context).ownMessageTheme, - reverse: widget.reverse, - ), + right: 8, left: 8, top: 8, bottom: hasNonUrlAttachments ? 8 : 0), ); } @@ -564,11 +561,10 @@ class _MessageWidgetState extends State { }; children.addAll([ - if (showSendingIndicator) _buildSendingIndicator(), if (showInChannel || showThreadReplyIndicator) ...[ if (showThreadParticipants) SizedBox.fromSize( - size: Size((threadParticipants.length * 8.0) + 10, 16), + size: Size((threadParticipants.length * 8.0) + 8, 16), child: _buildThreadParticipantsIndicator(), ), InkWell( @@ -588,16 +584,18 @@ class _MessageWidgetState extends State { Jiffy(widget.message.createdAt.toLocal()).jm, style: widget.messageTheme.createdAt, ), + if (showSendingIndicator) _buildSendingIndicator(), ]); - if (widget.reverse) children = children.reversed.toList(); + final showThreadTail = !(hasUrlAttachments || isGiphy || isOnlyEmoji) && + (showThreadReplyIndicator || showInChannel); return Flex( direction: Axis.horizontal, clipBehavior: Clip.none, crossAxisAlignment: CrossAxisAlignment.end, children: [ - if (showThreadReplyIndicator || showInChannel) + if (showThreadTail) Container( margin: EdgeInsets.only( bottom: widget.messageTheme.replies.fontSize / 2, @@ -648,18 +646,20 @@ class _MessageWidgetState extends State { var padding = 0.0; return Stack( children: widget.message.threadParticipants.map((user) { - padding += 10.0; + padding += 8.0; return Positioned( - left: padding - 10, + right: padding - 8, bottom: 0, top: 0, - child: Material( - color: Colors.white, - clipBehavior: Clip.antiAlias, - shape: CircleBorder(), + child: Container( + decoration: BoxDecoration( + shape: BoxShape.circle, + color: StreamChatTheme.of(context).colorTheme.white, + ), + padding: const EdgeInsets.all(1), child: UserAvatar( user: user, - constraints: BoxConstraints.loose(Size.fromRadius(8)), + constraints: BoxConstraints.loose(Size.fromRadius(7)), showOnlineStatus: false, ), ), @@ -694,6 +694,7 @@ class _MessageWidgetState extends State { flipTail: widget.reverse, backgroundColor: widget.messageTheme.reactionsBackgroundColor, borderColor: widget.messageTheme.reactionsBorderColor, + maskColor: widget.messageTheme.reactionsMaskColor, reactions: reactionsList, ), ) @@ -777,7 +778,7 @@ class _MessageWidgetState extends State { side: widget.attachmentBorderSide ?? widget.borderSide ?? BorderSide( - color: StreamChatTheme.of(context).colorTheme.greyGainsboro, + color: StreamChatTheme.of(context).colorTheme.greyWhisper, ), borderRadius: widget.attachmentBorderRadiusGeometry ?? widget.borderRadiusGeometry ?? @@ -785,7 +786,7 @@ class _MessageWidgetState extends State { ); } - List _parseAttachments(BuildContext context) { + Widget _parseAttachments(BuildContext context) { final images = widget.message.attachments ?.where((element) => element.type == 'image' && element.ogScrapeUrl == null) @@ -793,8 +794,9 @@ class _MessageWidgetState extends State { []; if (images.length > 1) { - return [ - wrapAttachmentWidget( + return Padding( + padding: widget.attachmentPadding, + child: wrapAttachmentWidget( context, Material( color: widget.messageTheme.messageBackgroundColor, @@ -809,30 +811,36 @@ class _MessageWidgetState extends State { ), ), ), - ]; + ); } - return widget.message.attachments - ?.where((element) => element.ogScrapeUrl == null) - ?.map((attachment) { - final attachmentBuilder = widget.attachmentBuilders[attachment.type]; + return Padding( + padding: widget.attachmentPadding, + child: Column( + mainAxisSize: MainAxisSize.min, + children: widget.message.attachments + ?.where((element) => element.ogScrapeUrl == null) + ?.map((attachment) { + final attachmentBuilder = + widget.attachmentBuilders[attachment.type]; - if (attachmentBuilder == null) { - return SizedBox(); - } - - final attachmentWidget = attachmentBuilder( - context, - widget.message, - attachment, - ); - return wrapAttachmentWidget( - context, - attachmentWidget, - attachment: attachment, - ); - })?.toList() ?? - []; + if (attachmentBuilder == null) return SizedBox(); + final attachmentWidget = attachmentBuilder( + context, + widget.message, + attachment, + ); + return wrapAttachmentWidget( + context, + attachmentWidget, + attachment: attachment, + ); + })?.insertBetween(SizedBox( + height: widget.attachmentPadding.vertical / 2, + )) ?? + [], + ), + ); } Widget wrapAttachmentWidget( @@ -843,21 +851,13 @@ class _MessageWidgetState extends State { final attachmentShape = widget.attachmentShape ?? widget.shape ?? _getDefaultShape(context); return Material( - color: _getBackgroundColor(), clipBehavior: Clip.antiAlias, shape: attachmentShape, - child: Padding( - padding: widget.attachmentPadding, - child: Material( - clipBehavior: Clip.hardEdge, - shape: attachmentShape, - type: MaterialType.transparency, - child: Transform( - transform: Matrix4.rotationY(widget.reverse ? pi : 0), - alignment: Alignment.center, - child: attachmentWidget, - ), - ), + type: MaterialType.transparency, + child: Transform( + transform: Matrix4.rotationY(widget.reverse ? pi : 0), + alignment: Alignment.center, + child: attachmentWidget, ), ); } @@ -944,11 +944,7 @@ class _MessageWidgetState extends State { : widget.messageTheme, ), ), - if (widget.message.attachments - ?.any((element) => element.ogScrapeUrl != null) == - true && - !hasQuotedMessage) - _buildUrlAttachment(), + if (hasUrlAttachments && !hasQuotedMessage) _buildUrlAttachment(), ], ), ); @@ -961,9 +957,7 @@ class _MessageWidgetState extends State { return widget.messageTheme.messageBackgroundColor; } - if (widget.message.attachments - ?.any((element) => element.ogScrapeUrl != null) == - true) { + if (hasUrlAttachments) { return StreamChatTheme.of(context).colorTheme.blueAlice; } diff --git a/packages/flutter_widgets/lib/src/quoted_message_widget.dart b/packages/flutter_widgets/lib/src/quoted_message_widget.dart index 309df703..e4e10e17 100644 --- a/packages/flutter_widgets/lib/src/quoted_message_widget.dart +++ b/packages/flutter_widgets/lib/src/quoted_message_widget.dart @@ -83,6 +83,8 @@ class QuotedMessageWidget extends StatelessWidget { final Map attachmentThumbnailBuilders; + final EdgeInsetsGeometry padding; + final GestureTapCallback onTap; /// @@ -94,6 +96,7 @@ class QuotedMessageWidget extends StatelessWidget { this.showBorder = false, this.textLimit = 170, this.attachmentThumbnailBuilders, + this.padding = const EdgeInsets.all(8), this.onTap, }) : super(key: key); @@ -107,16 +110,19 @@ class QuotedMessageWidget extends StatelessWidget { @override Widget build(BuildContext context) { - return InkWell( - onTap: onTap, - child: Row( - crossAxisAlignment: CrossAxisAlignment.end, - mainAxisSize: MainAxisSize.min, - children: [ - Flexible(child: _buildMessage(context)), - SizedBox(width: 8), - _buildUserAvatar(), - ], + return Padding( + padding: padding, + child: InkWell( + onTap: onTap, + child: Row( + crossAxisAlignment: CrossAxisAlignment.end, + mainAxisSize: MainAxisSize.min, + children: [ + Flexible(child: _buildMessage(context)), + SizedBox(width: 8), + _buildUserAvatar(), + ], + ), ), ); } diff --git a/packages/flutter_widgets/lib/src/reaction_bubble.dart b/packages/flutter_widgets/lib/src/reaction_bubble.dart index 9eee9997..74a2ae99 100644 --- a/packages/flutter_widgets/lib/src/reaction_bubble.dart +++ b/packages/flutter_widgets/lib/src/reaction_bubble.dart @@ -12,17 +12,21 @@ class ReactionBubble extends StatelessWidget { @required this.reactions, @required this.borderColor, @required this.backgroundColor, + @required this.maskColor, this.reverse = false, this.flipTail = false, this.highlightOwnReactions = true, + this.tailCirclesSpacing = 0, }) : super(key: key); final List reactions; final Color borderColor; final Color backgroundColor; + final Color maskColor; final bool reverse; final bool flipTail; final bool highlightOwnReactions; + final double tailCirclesSpacing; @override Widget build(BuildContext context) { @@ -38,51 +42,58 @@ class ReactionBubble extends StatelessWidget { Transform.translate( offset: Offset(reverse ? offset : -offset, 0), child: Container( - padding: EdgeInsets.symmetric( - vertical: 4, - horizontal: totalReactions > 1 ? 4 : 0, - ), + padding: const EdgeInsets.all(2), decoration: BoxDecoration( - border: Border.all( - color: borderColor, - ), - color: backgroundColor, - borderRadius: BorderRadius.all(Radius.circular(14)), + color: maskColor, + borderRadius: BorderRadius.all(Radius.circular(16)), ), - 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(), - ], - ); - }, + child: Container( + padding: EdgeInsets.symmetric( + vertical: 4, + horizontal: totalReactions > 1 ? 4 : 0, + ), + decoration: BoxDecoration( + border: Border.all( + color: borderColor, + ), + color: backgroundColor, + borderRadius: BorderRadius.all(Radius.circular(14)), + ), + 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(), + ], + ); + }, + ), ), ), ), Positioned( - bottom: 0, - left: reverse ? null : 11, - right: !reverse ? null : 11, + bottom: 2, + left: reverse ? null : 13, + right: !reverse ? null : 13, child: _buildReactionsTail(context), ), ], @@ -136,6 +147,8 @@ class ReactionBubble extends StatelessWidget { painter: ReactionBubblePainter( backgroundColor, borderColor, + maskColor, + tailCirclesSpace: tailCirclesSpacing, ), ); return Transform( @@ -149,14 +162,22 @@ class ReactionBubble extends StatelessWidget { class ReactionBubblePainter extends CustomPainter { final Color color; final Color borderColor; + final Color maskColor; + final double tailCirclesSpace; ReactionBubblePainter( this.color, this.borderColor, - ); + this.maskColor, { + this.tailCirclesSpace = 0, + }); @override void paint(Canvas canvas, Size size) { + _drawOvalMask(size, canvas); + + _drawMask(size, canvas); + _drawOval(size, canvas); _drawOvalBorder(size, canvas); @@ -166,6 +187,21 @@ class ReactionBubblePainter extends CustomPainter { _drawBorder(size, canvas); } + void _drawOvalMask(Size size, Canvas canvas) { + final paint = Paint() + ..color = maskColor + ..style = PaintingStyle.fill; + + final path = Path(); + path.addOval( + Rect.fromCircle( + center: Offset(4, 3) + Offset(tailCirclesSpace, tailCirclesSpace), + radius: 4, + ), + ); + canvas.drawPath(path, paint); + } + void _drawOvalBorder(Size size, Canvas canvas) { final paint = Paint() ..color = borderColor @@ -175,7 +211,7 @@ class ReactionBubblePainter extends CustomPainter { final path = Path(); path.addOval( Rect.fromCircle( - center: Offset(4, 3), + center: Offset(4, 3) + Offset(tailCirclesSpace, tailCirclesSpace), radius: 2, ), ); @@ -189,7 +225,7 @@ class ReactionBubblePainter extends CustomPainter { final path = Path(); path.addOval(Rect.fromCircle( - center: Offset(4, 3), + center: Offset(4, 3) + Offset(tailCirclesSpace, tailCirclesSpace), radius: 2, )); canvas.drawPath(path, paint); @@ -236,6 +272,27 @@ class ReactionBubblePainter extends CustomPainter { canvas.drawPath(path, paint); } + void _drawMask(Size size, Canvas canvas) { + final paint = Paint() + ..color = maskColor + ..strokeWidth = 1 + ..style = PaintingStyle.fill; + + final dy = -2.2; + final startAngle = 1.1; + final sweepAngle = 1.2; + final path = Path(); + path.addArc( + Rect.fromCircle( + center: Offset(1, dy), + radius: 6, + ), + -pi * startAngle, + -pi / sweepAngle, + ); + canvas.drawPath(path, paint); + } + @override bool shouldRepaint(CustomPainter oldDelegate) { return true; diff --git a/packages/flutter_widgets/lib/src/stream_chat_theme.dart b/packages/flutter_widgets/lib/src/stream_chat_theme.dart index c6d2d72f..f9c81be7 100644 --- a/packages/flutter_widgets/lib/src/stream_chat_theme.dart +++ b/packages/flutter_widgets/lib/src/stream_chat_theme.dart @@ -234,6 +234,7 @@ class StreamChatThemeData { messageBackgroundColor: colorTheme.greyGainsboro, reactionsBackgroundColor: colorTheme.white, reactionsBorderColor: colorTheme.greyWhisper, + reactionsMaskColor: colorTheme.whiteSnow, messageBorderColor: colorTheme.greyGainsboro, avatarTheme: AvatarTheme( borderRadius: BorderRadius.circular(20), @@ -249,6 +250,7 @@ class StreamChatThemeData { otherMessageTheme: MessageTheme( reactionsBackgroundColor: colorTheme.greyGainsboro, reactionsBorderColor: colorTheme.white, + reactionsMaskColor: colorTheme.whiteSnow, messageText: textTheme.body, createdAt: textTheme.footnote.copyWith(color: colorTheme.grey), replies: textTheme.footnoteBold.copyWith(color: accentColor), @@ -489,9 +491,9 @@ class ColorTheme { stops: [0, 1], ), this.borderTop = const Effect( - sigmaX: 0, sigmaY: -1, color: Color(0xff141924), blur: 0.0), + sigmaX: 0, sigmaY: -1, color: Color(0xff000000), blur: 0.0), this.borderBottom = - const Effect(sigmaX: 0, sigmaY: 1, color: Color(0xff141924), blur: 0.0), + const Effect(sigmaX: 0, sigmaY: 1, color: Color(0xff000000), blur: 0.0), this.shadowIconButton = const Effect( sigmaX: 0, sigmaY: 2, color: Color(0xff000000), alpha: 0.5, blur: 4.0), this.modalShadow = const Effect( @@ -710,6 +712,7 @@ class MessageTheme { final Color messageBorderColor; final Color reactionsBackgroundColor; final Color reactionsBorderColor; + final Color reactionsMaskColor; final AvatarTheme avatarTheme; const MessageTheme({ @@ -721,6 +724,7 @@ class MessageTheme { this.messageBorderColor, this.reactionsBackgroundColor, this.reactionsBorderColor, + this.reactionsMaskColor, this.avatarTheme, this.createdAt, }); @@ -736,6 +740,7 @@ class MessageTheme { AvatarTheme avatarTheme, Color reactionsBackgroundColor, Color reactionsBorderColor, + Color reactionsMaskColor, }) => MessageTheme( messageText: messageText ?? this.messageText, @@ -750,6 +755,7 @@ class MessageTheme { reactionsBackgroundColor: reactionsBackgroundColor ?? this.reactionsBackgroundColor, reactionsBorderColor: reactionsBorderColor ?? this.reactionsBorderColor, + reactionsMaskColor: reactionsMaskColor ?? this.reactionsMaskColor, ); MessageTheme merge(MessageTheme other) { @@ -767,6 +773,7 @@ class MessageTheme { avatarTheme: avatarTheme?.merge(other.avatarTheme) ?? other.avatarTheme, reactionsBackgroundColor: other.reactionsBackgroundColor, reactionsBorderColor: other.reactionsBorderColor, + reactionsMaskColor: other.reactionsMaskColor, ); } } diff --git a/packages/flutter_widgets/lib/src/user_avatar.dart b/packages/flutter_widgets/lib/src/user_avatar.dart index 688eff77..706c4d7e 100644 --- a/packages/flutter_widgets/lib/src/user_avatar.dart +++ b/packages/flutter_widgets/lib/src/user_avatar.dart @@ -92,18 +92,18 @@ class UserAvatar extends StatelessWidget { child: Material( type: MaterialType.circle, child: Container( - padding: const EdgeInsets.all(2.0), + margin: const EdgeInsets.all(2.0), constraints: onlineIndicatorConstraints ?? BoxConstraints.tightFor( - width: 12, - height: 12, + width: 8, + height: 8, ), child: Material( shape: CircleBorder(), color: streamChatTheme.colorTheme.accentGreen, ), ), - color: StreamChatTheme.of(context).colorTheme.white, + color: streamChatTheme.colorTheme.white, ), ), ), diff --git a/packages/flutter_widgets/pubspec.yaml b/packages/flutter_widgets/pubspec.yaml index e146154a..c0d16cb0 100644 --- a/packages/flutter_widgets/pubspec.yaml +++ b/packages/flutter_widgets/pubspec.yaml @@ -28,8 +28,8 @@ dependencies: file_picker: ^2.1.5 image_picker: ^0.6.7+17 flutter_keyboard_visibility: ^4.0.2 - stream_chat: ^0.2.23+2 mime: ^0.9.7 + stream_chat: ^0.2.23+3 video_compress: ^2.1.1 visibility_detector: ^0.1.5 http_parser: ^3.1.4 diff --git a/packages/flutter_widgets/test/src/reaction_bubble_test.dart b/packages/flutter_widgets/test/src/reaction_bubble_test.dart index c0bc8cd1..0b6c90ef 100644 --- a/packages/flutter_widgets/test/src/reaction_bubble_test.dart +++ b/packages/flutter_widgets/test/src/reaction_bubble_test.dart @@ -19,6 +19,7 @@ void main() { reactions: [], borderColor: Colors.black, backgroundColor: Colors.white, + maskColor: Colors.white, ), ), ), @@ -56,6 +57,7 @@ void main() { ], borderColor: Colors.black, backgroundColor: Colors.white, + maskColor: Colors.white, ), ), ), @@ -96,6 +98,7 @@ void main() { ], borderColor: Colors.black, backgroundColor: Colors.white, + maskColor: Colors.white, ), ), ),