From 903dec9d1fdeb1b305a5f17e6164a943fba55d4a Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Mon, 21 Dec 2020 15:29:59 +0530 Subject: [PATCH 01/10] [Thread Header] Redesign wrt new ui Signed-off-by: Sahil Kumar --- lib/src/thread_header.dart | 79 +++++++++++++++++++++----------------- 1 file changed, 43 insertions(+), 36 deletions(-) diff --git a/lib/src/thread_header.dart b/lib/src/thread_header.dart index aa14cb61..cfb6b1f1 100644 --- a/lib/src/thread_header.dart +++ b/lib/src/thread_header.dart @@ -1,7 +1,9 @@ import 'package:flutter/material.dart'; import 'package:stream_chat/stream_chat.dart'; import 'package:stream_chat_flutter/src/stream_chat_theme.dart'; -import 'package:stream_chat_flutter/src/stream_svg_icon.dart'; + +import 'back_button.dart'; +import 'channel_name.dart'; /// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/screenshots/thread_header.png) /// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/screenshots/thread_header_paint.png) @@ -77,43 +79,48 @@ class ThreadHeader extends StatelessWidget implements PreferredSizeWidget { return AppBar( automaticallyImplyLeading: false, elevation: 1, + leading: showBackButton + ? StreamBackButton( + onPressed: onBackPressed, + showUnreads: true, + ) + : SizedBox(), backgroundColor: StreamChatTheme.of(context).channelTheme.channelHeaderTheme.color, - actions: [ - Container( - child: showBackButton - ? AspectRatio( - aspectRatio: 1, - child: IconButton( - onPressed: onBackPressed ?? () => Navigator.pop(context), - icon: StreamSvgIcon.close( - size: 24, - color: Theme.of(context).brightness == Brightness.dark - ? Colors.white - : Colors.black, - ), - ), - ) - : SizedBox(), - ), - ], - centerTitle: false, - title: Text.rich( - TextSpan( - text: 'Thread', - children: [ - TextSpan( - text: - ' ${parent.replyCount} ${parent.replyCount == 1 ? 'reply' : 'replies'}', - style: StreamChatTheme.of(context) - .channelTheme - .channelHeaderTheme - .lastMessageAt, - ), - ], - ), - style: - StreamChatTheme.of(context).channelTheme.channelHeaderTheme.title, + centerTitle: true, + title: Column( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + 'Thread Reply', + style: StreamChatTheme.of(context) + .channelTheme + .channelHeaderTheme + .title, + ), + SizedBox(height: 2), + Row( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + 'with ', + style: StreamChatTheme.of(context) + .channelTheme + .channelHeaderTheme + .lastMessageAt, + ), + ChannelName( + textStyle: StreamChatTheme.of(context) + .channelTheme + .channelHeaderTheme + .lastMessageAt, + ), + ], + ), + ], ), ); } From a4071c117e2e9a2b5cd3b93aca24318ce2e6b207 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Mon, 21 Dec 2020 15:31:13 +0530 Subject: [PATCH 02/10] [Message Input] Change "sendAsDm" checkbox title Signed-off-by: Sahil Kumar --- lib/src/message_input.dart | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/src/message_input.dart b/lib/src/message_input.dart index dec2b033..c46c899b 100644 --- a/lib/src/message_input.dart +++ b/lib/src/message_input.dart @@ -21,7 +21,6 @@ import 'package:stream_chat_flutter/src/stream_svg_icon.dart'; import 'package:stream_chat_flutter/src/user_avatar.dart'; import 'package:substring_highlight/substring_highlight.dart'; import 'package:video_compress/video_compress.dart'; -import 'package:photo_manager/photo_manager.dart'; import '../stream_chat_flutter.dart'; import 'stream_channel.dart'; @@ -302,7 +301,7 @@ class MessageInputState extends State { ), Padding( padding: const EdgeInsets.symmetric(horizontal: 16.0), - child: Text('Send also as direct message'), + child: Text('Also send as direct message'), ), ], ), From 209c5a3c24766e9ee472870b98320c3e63753f6c Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Tue, 22 Dec 2020 21:45:14 +0530 Subject: [PATCH 03/10] Initial implementation Signed-off-by: Sahil Kumar --- example/lib/main.dart | 1 + lib/src/message_actions_modal.dart | 2 +- lib/src/message_list_view.dart | 34 +- lib/src/message_reactions_modal.dart | 2 +- lib/src/message_widget.dart | 535 +++++++++++++++------------ lib/src/reply_indicator.dart | 56 --- lib/src/stream_channel.dart | 13 + lib/src/stream_chat_theme.dart | 19 +- lib/src/utils.dart | 9 + lib/stream_chat_flutter.dart | 1 - 10 files changed, 359 insertions(+), 313 deletions(-) delete mode 100644 lib/src/reply_indicator.dart diff --git a/example/lib/main.dart b/example/lib/main.dart index f0f1e59b..803df8e6 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -470,6 +470,7 @@ class ThreadPage extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( + backgroundColor: Color.fromRGBO(252, 252, 252, 1), appBar: ThreadHeader( parent: parent, ), diff --git a/lib/src/message_actions_modal.dart b/lib/src/message_actions_modal.dart index 1967cd2b..f4cd47c8 100644 --- a/lib/src/message_actions_modal.dart +++ b/lib/src/message_actions_modal.dart @@ -113,7 +113,7 @@ class MessageActionsModal extends StatelessWidget { messageTheme: messageTheme, showReactions: false, showUsername: false, - showReplyIndicator: false, + showThreadReplyIndicator: false, showUserAvatar: showUserAvatar, showTimestamp: false, translateUserAvatar: false, diff --git a/lib/src/message_list_view.dart b/lib/src/message_list_view.dart index 5a850ad3..24d32d88 100644 --- a/lib/src/message_list_view.dart +++ b/lib/src/message_list_view.dart @@ -241,7 +241,7 @@ class _MessageListViewState extends State { builder: (context, snapshot) { if (!snapshot.hasData) { return Center( - child: CircularProgressIndicator(), + child: const CircularProgressIndicator(), ); } @@ -336,18 +336,25 @@ class _MessageListViewState extends State { crossAxisAlignment: CrossAxisAlignment.stretch, children: [ buildParentMessage(widget.parentMessage), - Padding( - padding: - const EdgeInsets.symmetric(horizontal: 32), - child: Container( - padding: const EdgeInsets.all(8), + Container( + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [ + Color(0XFFF7F7F7), + Color(0XFFFCFCFC), + ], + ), + ), + child: Padding( + padding: const EdgeInsets.all(8.0), child: Text( - 'Start of thread', + '${widget.parentMessage.replyCount} ${widget.parentMessage.replyCount == 1 ? 'Reply' : 'Replies'}', textAlign: TextAlign.center, + style: StreamChatTheme.of(context) + .channelTheme + .channelHeaderTheme + .lastMessageAt, ), - color: Theme.of(context) - .accentColor - .withAlpha(50), ), ), ], @@ -585,7 +592,7 @@ class _MessageListViewState extends State { if (!snapshot.data) { if (direction == QueryDirection.top) { return Container( - height: 50, + height: 52, width: double.infinity, ); } @@ -679,7 +686,8 @@ class _MessageListViewState extends State { final isMyMessage = message.user.id == StreamChat.of(context).user.id; return MessageWidget( - showReplyIndicator: false, + showThreadReplyIndicator: false, + showInChannelIndicator: false, message: message, reverse: isMyMessage, showUsername: !isMyMessage, @@ -748,6 +756,8 @@ class _MessageListViewState extends State { bottom: index == 0 ? 30 : (isNextUser ? 2 : 7), top: 3, ), + showInChannelIndicator: widget.parentMessage == null, + showThreadReplyIndicator: widget.parentMessage == null, showUsername: !isMyMessage && !isNextUser, showSendingIndicator: isMyMessage && (index == 0 || message.status != MessageSendingStatus.SENT) diff --git a/lib/src/message_reactions_modal.dart b/lib/src/message_reactions_modal.dart index db5d8710..e5043e62 100644 --- a/lib/src/message_reactions_modal.dart +++ b/lib/src/message_reactions_modal.dart @@ -105,7 +105,7 @@ class MessageReactionsModal extends StatelessWidget { showReactions: false, showUsername: false, showUserAvatar: showUserAvatar, - showReplyIndicator: false, + showThreadReplyIndicator: false, showTimestamp: false, translateUserAvatar: false, showSendingIndicator: DisplayWidget.gone, diff --git a/lib/src/message_widget.dart b/lib/src/message_widget.dart index c7c6acdb..4fe84311 100644 --- a/lib/src/message_widget.dart +++ b/lib/src/message_widget.dart @@ -99,8 +99,11 @@ class MessageWidget extends StatefulWidget { final bool allRead; - /// If true the widget will show the reply indicator - final bool showReplyIndicator; + /// If true the widget will show the thread reply indicator + final bool showThreadReplyIndicator; + + /// If true the widget will show the show in channel indicator + final bool showInChannelIndicator; /// The function called when tapping on UserAvatar final void Function(User) onUserAvatarTap; @@ -123,6 +126,7 @@ class MessageWidget extends StatefulWidget { /// Center user avatar with bottom of the message final bool translateUserAvatar; + /// MessageWidget({ Key key, @required this.message, @@ -139,7 +143,8 @@ class MessageWidget extends StatefulWidget { this.showReactionPickerIndicator = false, this.showUserAvatar = DisplayWidget.show, this.showSendingIndicator = DisplayWidget.show, - this.showReplyIndicator = true, + this.showThreadReplyIndicator = true, + this.showInChannelIndicator = true, this.onThreadTap, this.showUsername = true, this.showTimestamp = true, @@ -211,10 +216,23 @@ class MessageWidget extends StatefulWidget { } class _MessageWidgetState extends State { + bool get showThreadReplyIndicator => + widget.showThreadReplyIndicator && widget.message.replyCount > 0; + + bool get showUsername => widget.showUsername; + + bool get showTimeStamp => + widget.message.createdAt != null && widget.showTimestamp; + + bool get showReadList => widget.readList?.isNotEmpty == true; + + bool get showInChannel => + widget.showInChannelIndicator && widget.message?.showInChannel == true; + @override Widget build(BuildContext context) { var leftPadding = widget.showUserAvatar != DisplayWidget.gone - ? widget.messageTheme.avatarTheme.constraints.maxWidth + 16.0 + ? widget.messageTheme.avatarTheme.constraints.maxWidth + 14.5 : 6.0; final hasFiles = @@ -234,140 +252,151 @@ class _MessageWidgetState extends State { crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ - Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.end, + Stack( + alignment: AlignmentDirectional.bottomStart, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, - children: [ - if (widget.showUserAvatar == DisplayWidget.show) - _buildUserAvatar(), - SizedBox( - width: 6, - ), - if (widget.showUserAvatar == DisplayWidget.hide) - SizedBox( - width: widget.messageTheme.avatarTheme.constraints - .maxWidth + - 8, - ), - Flexible( - child: PortalEntry( - portal: Container( - transform: Matrix4.translationValues(-16, 2, 0), - child: _buildReactionIndicator(context), - constraints: BoxConstraints(maxWidth: 22 * 6.0), - ), - portalAnchor: Alignment(-1.0, -1.0), - childAnchor: Alignment(1, -1.0), - child: Stack( - clipBehavior: Clip.none, - children: [ - Padding( - padding: widget.showReactions - ? EdgeInsets.only( - top: widget.message.reactionCounts - ?.isNotEmpty == - true - ? 18 - : 0, - ) - : EdgeInsets.zero, - child: (widget.message.isDeleted && - widget.message.status != - MessageSendingStatus - .FAILED_DELETE) - ? Transform( - alignment: Alignment.center, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.end, + mainAxisSize: MainAxisSize.min, + children: [ + if (widget.showUserAvatar == DisplayWidget.show) + _buildUserAvatar(), + SizedBox(width: 6), + if (widget.showUserAvatar == DisplayWidget.hide) + SizedBox( + width: widget.messageTheme.avatarTheme + .constraints.maxWidth + + 8, + ), + Flexible( + child: PortalEntry( + portal: Container( + transform: + Matrix4.translationValues(-16, 2, 0), + child: _buildReactionIndicator(context), + constraints: + BoxConstraints(maxWidth: 22 * 6.0), + ), + portalAnchor: Alignment(-1.0, -1.0), + childAnchor: Alignment(1, -1.0), + child: Stack( + clipBehavior: Clip.none, + children: [ + Padding( + padding: widget.showReactions + ? EdgeInsets.only( + top: widget.message.reactionCounts + ?.isNotEmpty == + true + ? 18 + : 0, + ) + : EdgeInsets.zero, + child: (widget.message.isDeleted && + widget.message.status != + MessageSendingStatus + .FAILED_DELETE) + ? Transform( + alignment: Alignment.center, + transform: Matrix4.rotationY( + widget.reverse ? pi : 0), + child: DeletedMessage( + reverse: widget.reverse, + borderRadiusGeometry: + widget.borderRadiusGeometry, + borderSide: widget.borderSide, + shape: widget.shape, + messageTheme: + widget.messageTheme, + ), + ) + : Material( + clipBehavior: Clip.antiAlias, + shape: widget.shape ?? + RoundedRectangleBorder( + side: isOnlyEmoji + ? BorderSide.none + : widget.borderSide ?? + BorderSide( + color: Theme.of(context) + .brightness == + Brightness + .dark + ? Colors.white + .withAlpha( + 24) + : Colors.black + .withAlpha( + 24), + ), + borderRadius: widget + .borderRadiusGeometry ?? + BorderRadius.zero, + ), + color: _getBackgroundColor(), + child: Padding( + padding: EdgeInsets.all( + hasFiles ? 2.0 : 0.0), + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + mainAxisSize: + MainAxisSize.min, + children: [ + ..._parseAttachments( + context), + if (widget.message.text + .trim() + .isNotEmpty && + !isGiphy) + _buildTextBubble(context), + ], + ), + ), + ), + ), + if (widget.showReactionPickerIndicator) + Positioned( + right: 0, + top: -6, + child: Transform( transform: Matrix4.rotationY( widget.reverse ? pi : 0), - child: DeletedMessage( - reverse: widget.reverse, - borderRadiusGeometry: - widget.borderRadiusGeometry, - borderSide: widget.borderSide, - shape: widget.shape, - messageTheme: widget.messageTheme, - ), - ) - : Material( - clipBehavior: Clip.antiAlias, - shape: widget.shape ?? - RoundedRectangleBorder( - side: isOnlyEmoji - ? BorderSide.none - : widget.borderSide ?? - BorderSide( - color: Theme.of(context) - .brightness == - Brightness - .dark - ? Colors.white - .withAlpha(24) - : Colors.black - .withAlpha( - 24), - ), - borderRadius: widget - .borderRadiusGeometry ?? - BorderRadius.zero, - ), - color: _getBackgroundColor(), - child: Padding( - padding: EdgeInsets.all( - hasFiles ? 2.0 : 0.0), - child: Column( - crossAxisAlignment: - CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - ..._parseAttachments(context), - if (widget.message.text - .trim() - .isNotEmpty && - !isGiphy) - _buildTextBubble(context), - ], + child: CustomPaint( + painter: ReactionBubblePainter( + widget.messageTheme + .reactionsBackgroundColor, + widget.messageTheme + .reactionsBorderColor, ), ), ), - ), - if (widget.showReactionPickerIndicator) - Positioned( - right: 0, - top: -6, - child: Transform( - transform: Matrix4.rotationY( - widget.reverse ? pi : 0), - child: CustomPaint( - painter: ReactionBubblePainter( - widget.messageTheme - .reactionsBackgroundColor, - widget.messageTheme - .reactionsBorderColor, - ), ), - ), - ), - ], + ], + ), + ), ), - ), + ], ), + if (showThreadReplyIndicator || + showUsername || + showTimeStamp || + showInChannel) + SizedBox(height: 20.0), ], ), - if (widget.showReplyIndicator && - widget.message.replyCount > 0) - _buildReplyIndicator(leftPadding), + if (showThreadReplyIndicator || + showUsername || + showTimeStamp || + showInChannel) + _buildBottomRows(leftPadding) ], ), - if ((widget.message.createdAt != null && - widget.showTimestamp) || - widget.showUsername || - widget.readList?.isNotEmpty == true) - _buildBottomRow(leftPadding), ], ), ), @@ -376,6 +405,113 @@ class _MessageWidgetState extends State { ); } + Widget _buildBottomRows(double leftPadding) { + final deleted = widget.message.isDeleted; + var children = []; + if (deleted) { + children.add( + Row( + mainAxisSize: MainAxisSize.min, + children: [ + StreamSvgIcon.eye( + color: Colors.black.withOpacity(0.5), + size: 16.0, + ), + SizedBox(width: 8.0), + Text( + 'Only visible to you', + style: TextStyle( + color: Colors.black.withOpacity(0.5), + fontSize: 12.0, + ), + ), + ], + ), + ); + } else { + final showSendingIndicator = + widget.showSendingIndicator == DisplayWidget.show; + final replyCount = widget.message.replyCount; + final msg = showInChannel + ? 'Thread Reply' + : replyCount != 0 + ? '$replyCount ${replyCount > 1 ? 'Thread Replies' : 'Thread Reply'}' + : 'Thread Reply'; + + final onThreadTap = () async { + try { + var message = widget.message; + if (showInChannel && message.parentId != null) { + final channel = StreamChannel.of(context); + message = await channel.getMessage(widget.message.parentId); + } + return widget.onThreadTap(message); + } catch (e, stk) { + print(e); + print(stk); + return null; + } + }; + + children.addAll([ + if (showSendingIndicator) _buildSendingIndicator(), + if (showReadList) + SizedBox.fromSize( + size: Size((widget.readList.length * 10.0) + 10, 17), + child: Padding( + padding: const EdgeInsets.only(left: 4.0), + child: _buildReadIndicator(), + ), + ), + if (showThreadReplyIndicator || showInChannel) + InkWell( + onTap: widget.onThreadTap != null ? onThreadTap : null, + child: Text(msg, style: widget.messageTheme?.replies), + ), + if (showUsername) + Text( + widget.message.user.name, + style: widget.messageTheme.replies.copyWith( + color: widget.messageTheme.createdAt.color, + ), + ), + if (showTimeStamp) + Text( + Jiffy(widget.message.createdAt.toLocal()).jm, + style: widget.messageTheme.createdAt, + ), + ]); + } + if (widget.reverse) children = children.reversed.toList(); + + return Padding( + padding: EdgeInsets.only(left: leftPadding), + child: Row( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + if (!deleted && (showThreadReplyIndicator || showInChannel)) + Container( + margin: EdgeInsets.only( + bottom: widget.messageTheme.replies.fontSize / 2), + child: CustomPaint( + size: const Size(16, 32), + painter: _ThreadReplyPainter( + color: widget.messageTheme.replyThreadColor, + ), + ), + ), + ...children.map( + (child) => Transform( + transform: Matrix4.rotationY(widget.reverse ? pi : 0), + alignment: Alignment.center, + child: child, + ), + ), + ].insertBetween(const SizedBox(width: 8.0)), + ), + ); + } + Widget _buildUrlAttachment() { var urlAttachment = widget.message.attachments .firstWhere((element) => element.ogScrapeUrl != null); @@ -394,87 +530,6 @@ class _MessageWidgetState extends State { ); } - Padding _buildBottomRow(double leftPadding) { - return Padding( - padding: EdgeInsets.only( - left: leftPadding, - top: 2, - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - Transform( - alignment: Alignment.center, - transform: Matrix4.rotationY(widget.reverse ? pi : 0), - child: RichText( - text: TextSpan( - style: widget.messageTheme.createdAt, - children: [ - if (widget.showUsername) - TextSpan( - text: widget.message.user.name, - style: TextStyle( - fontWeight: FontWeight.bold, - color: widget.messageTheme.createdAt.color - .withOpacity(1)), - ), - if (widget.message.createdAt != null && widget.showTimestamp) - TextSpan( - text: Jiffy(widget.message.createdAt.toLocal()) - .format(' HH:mm'), - ), - ], - ), - ), - ), - if (widget.showSendingIndicator == DisplayWidget.show) - _buildSendingIndicator(), - if (widget.readList?.isNotEmpty == true) - SizedBox.fromSize( - size: Size((widget.readList.length * 10.0) + 10, 17), - child: Transform( - alignment: Alignment.center, - transform: Matrix4.rotationY(widget.reverse ? pi : 0), - child: Padding( - padding: const EdgeInsets.only(left: 4.0), - child: _buildReadIndicator(), - ), - ), - ), - if (widget.message.isDeleted) - Transform( - transform: Matrix4.rotationY(widget.reverse ? pi : 0), - alignment: Alignment.center, - child: Padding( - padding: - const EdgeInsets.symmetric(horizontal: 8.0, vertical: 4.0), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - StreamSvgIcon.eye( - color: Colors.black.withOpacity(0.5), - size: 16.0, - ), - SizedBox( - width: 8.0, - ), - Text( - 'Only visible to you', - style: TextStyle( - color: Colors.black.withOpacity(0.5), - fontSize: 12.0, - ), - ), - ], - ), - ), - ), - ], - ), - ); - } - bool get isGiphy => widget.message.attachments?.any((element) => element.type == 'giphy') == true; @@ -565,7 +620,7 @@ class _MessageWidgetState extends State { true, showReactions: widget.showReactions, showReply: - widget.showReplyIndicator && widget.onThreadTap != null, + widget.showThreadReplyIndicator && widget.onThreadTap != null, ), ); }); @@ -715,38 +770,13 @@ class _MessageWidgetState extends State { return; } - Widget _buildReplyIndicator(double leftPadding) { - return Padding( - padding: EdgeInsets.only( - left: leftPadding, - ), - child: Transform( - transform: Matrix4.rotationY(widget.reverse ? pi : 0), - alignment: Alignment.center, - child: ReplyIndicator( - message: widget.message, - reversed: widget.reverse, - messageTheme: widget.messageTheme, - onTap: widget.onThreadTap != null - ? () { - widget.onThreadTap(widget.message); - } - : null, - ), - ), - ); - } - Widget _buildSendingIndicator() { - return Padding( - padding: const EdgeInsets.only(right: 4.0), - child: Transform( - transform: Matrix4.rotationY(widget.reverse ? pi : 0), - alignment: Alignment.center, - child: SendingIndicator( - message: widget.message, - allRead: widget.allRead, - ), + return Container( + height: widget.messageTheme.createdAt.fontSize + 2, + width: widget.messageTheme.createdAt.fontSize + 2, + child: SendingIndicator( + message: widget.message, + allRead: widget.allRead, ), ); } @@ -958,3 +988,32 @@ class _MessageWidgetState extends State { } } } + +class _ThreadReplyPainter extends CustomPainter { + final Color color; + + const _ThreadReplyPainter({@required this.color}); + + @override + void paint(Canvas canvas, Size size) { + final paint = Paint() + ..color = color ?? Color(0XFFDBDBDB) + ..style = PaintingStyle.stroke + ..strokeWidth = 1 + ..strokeCap = StrokeCap.round; + + final path = Path() + ..moveTo(0, 0) + ..quadraticBezierTo(0, size.height * 0.38, 0, size.height * 0.50) + ..quadraticBezierTo( + 0, + size.height, + size.width, + size.height, + ); + canvas.drawPath(path, paint); + } + + @override + bool shouldRepaint(covariant CustomPainter oldDelegate) => false; +} diff --git a/lib/src/reply_indicator.dart b/lib/src/reply_indicator.dart deleted file mode 100644 index cdb1cd57..00000000 --- a/lib/src/reply_indicator.dart +++ /dev/null @@ -1,56 +0,0 @@ -import 'dart:math'; - -import 'package:flutter/material.dart'; -import 'package:stream_chat/stream_chat.dart'; -import 'package:stream_chat_flutter/stream_chat_flutter.dart'; - -/// A reply button indicator -class ReplyIndicator extends StatelessWidget { - final Message message; - final VoidCallback onTap; - final bool reversed; - final MessageTheme messageTheme; - - const ReplyIndicator({ - Key key, - this.message, - this.onTap, - this.reversed = false, - this.messageTheme, - }) : super(key: key); - - @override - Widget build(BuildContext context) { - var row = [ - Text( - 'Replies: ${message.replyCount}', - style: messageTheme?.replies, - ), - Transform( - transform: Matrix4.rotationY(reversed ? 0 : pi), - alignment: Alignment.center, - child: Icon( - Icons.subdirectory_arrow_left, - color: Theme.of(context).brightness == Brightness.dark - ? Colors.white12 - : Colors.black12, - ), - ), - ]; - - if (!reversed) { - row = row.reversed.toList(); - } - - return GestureDetector( - onTap: onTap, - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 2.0), - child: Row( - mainAxisSize: MainAxisSize.min, - children: row, - ), - ), - ); - } -} diff --git a/lib/src/stream_channel.dart b/lib/src/stream_channel.dart index 001d98ff..d5a4b030 100644 --- a/lib/src/stream_channel.dart +++ b/lib/src/stream_channel.dart @@ -267,6 +267,19 @@ class StreamChannelState extends State { return state; } + /// + Future getMessage(String messageId) async { + var message = channel.state.messages.firstWhere( + (it) => it.id == messageId, + orElse: () => null, + ); + if (message == null) { + final response = await channel.getMessagesById([messageId]); + message = response.messages.first; + } + return message; + } + /// Reloads the channel with latest message Future reloadChannel() => queryAtMessage(before: 30); diff --git a/lib/src/stream_chat_theme.dart b/lib/src/stream_chat_theme.dart index 54b096d1..a0f552c4 100644 --- a/lib/src/stream_chat_theme.dart +++ b/lib/src/stream_chat_theme.dart @@ -191,6 +191,8 @@ class StreamChatThemeData { this.ownMessageTheme.messageBackgroundColor, avatarTheme: ownMessageTheme?.avatarTheme ?? this.ownMessageTheme.avatarTheme, + replyThreadColor: ownMessageTheme?.replyThreadColor ?? + this.ownMessageTheme.replyThreadColor, ) ?? this.ownMessageTheme, otherMessageTheme: otherMessageTheme?.copyWith( @@ -209,6 +211,8 @@ class StreamChatThemeData { this.otherMessageTheme.messageBackgroundColor, avatarTheme: otherMessageTheme?.avatarTheme ?? this.otherMessageTheme.avatarTheme, + replyThreadColor: ownMessageTheme?.replyThreadColor ?? + this.ownMessageTheme.replyThreadColor, ) ?? this.otherMessageTheme, reactionIcons: reactionIcons ?? this.reactionIcons, @@ -297,16 +301,17 @@ class StreamChatThemeData { color: isDark ? Colors.white.withOpacity(.5) : Colors.black.withOpacity(.5), - fontSize: 11, + fontSize: 12, ), replies: TextStyle( color: accentColor, - fontWeight: FontWeight.bold, + fontWeight: FontWeight.w600, fontSize: 12, ), messageBackgroundColor: isDark ? Color(0xff191919) : Color(0xffEAEAEA), reactionsBackgroundColor: isDark ? Colors.black : Colors.white, reactionsBorderColor: isDark ? Color(0xff191919) : Color(0xffEAEAEA), + replyThreadColor: isDark ? Color(0xff191919) : Color(0xffEAEAEA), avatarTheme: AvatarTheme( borderRadius: BorderRadius.circular(20), constraints: BoxConstraints.tightFor( @@ -330,17 +335,19 @@ class StreamChatThemeData { color: isDark ? Colors.white.withOpacity(.5) : Colors.black.withOpacity(.5), - fontSize: 11, + fontSize: 12, ), replies: TextStyle( color: accentColor, - fontWeight: FontWeight.bold, + fontWeight: FontWeight.w600, fontSize: 12, ), messageLinks: TextStyle( color: accentColor, ), messageBackgroundColor: isDark ? Colors.black : Colors.white, + replyThreadColor: + isDark ? Colors.white.withAlpha(24) : Colors.black.withAlpha(24), avatarTheme: AvatarTheme( borderRadius: BorderRadius.circular(20), constraints: BoxConstraints.tightFor( @@ -455,6 +462,7 @@ class MessageTheme { final Color messageBackgroundColor; final Color reactionsBackgroundColor; final Color reactionsBorderColor; + final Color replyThreadColor; final AvatarTheme avatarTheme; const MessageTheme({ @@ -465,6 +473,7 @@ class MessageTheme { this.messageBackgroundColor, this.reactionsBackgroundColor, this.reactionsBorderColor, + this.replyThreadColor, this.avatarTheme, this.createdAt, }); @@ -479,6 +488,7 @@ class MessageTheme { AvatarTheme avatarTheme, Color reactionsBackgroundColor, Color reactionsBorderColor, + Color replyThreadColor, }) => MessageTheme( messageText: messageText ?? this.messageText, @@ -492,6 +502,7 @@ class MessageTheme { reactionsBackgroundColor: reactionsBackgroundColor ?? this.reactionsBackgroundColor, reactionsBorderColor: reactionsBorderColor ?? this.reactionsBorderColor, + replyThreadColor: replyThreadColor ?? this.replyThreadColor, ); } diff --git a/lib/src/utils.dart b/lib/src/utils.dart index 74b1da64..681a28aa 100644 --- a/lib/src/utils.dart +++ b/lib/src/utils.dart @@ -92,3 +92,12 @@ Future showConfirmationDialog( /// Get random png with initials String getRandomPicUrl(User user) => 'https://getstream.io/random_png/?id=${user.id}&name=${user.name}'; + +/// List extension +extension ListX on List { + /// Insert any item inBetween the list items + List insertBetween(T item) => expand((e) sync* { + yield item; + yield e; + }).skip(1).toList(growable: false); +} diff --git a/lib/stream_chat_flutter.dart b/lib/stream_chat_flutter.dart index 5dd46fa3..1cef26a6 100644 --- a/lib/stream_chat_flutter.dart +++ b/lib/stream_chat_flutter.dart @@ -21,7 +21,6 @@ export 'src/message_list_view.dart'; export 'src/message_text.dart'; export 'src/message_widget.dart'; export 'src/reaction_picker.dart'; -export 'src/reply_indicator.dart'; export 'src/sending_indicator.dart'; export 'src/stream_channel.dart'; export 'src/stream_chat.dart'; From 011b939c282596d6318934155cd05ce794d79fc0 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Wed, 23 Dec 2020 13:58:02 +0530 Subject: [PATCH 04/10] [Thread Header] Fix channelName overflow Signed-off-by: Sahil Kumar --- lib/src/thread_header.dart | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/lib/src/thread_header.dart b/lib/src/thread_header.dart index cfb6b1f1..8e165af7 100644 --- a/lib/src/thread_header.dart +++ b/lib/src/thread_header.dart @@ -112,11 +112,13 @@ class ThreadHeader extends StatelessWidget implements PreferredSizeWidget { .channelHeaderTheme .lastMessageAt, ), - ChannelName( - textStyle: StreamChatTheme.of(context) - .channelTheme - .channelHeaderTheme - .lastMessageAt, + Flexible( + child: ChannelName( + textStyle: StreamChatTheme.of(context) + .channelTheme + .channelHeaderTheme + .lastMessageAt, + ), ), ], ), From c2633cecb253fa0c8dfca5abf8f303d487925a7a Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Wed, 23 Dec 2020 14:02:18 +0530 Subject: [PATCH 05/10] fix typo Signed-off-by: Sahil Kumar --- lib/src/message_widget.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/src/message_widget.dart b/lib/src/message_widget.dart index 4fe84311..c52e7f63 100644 --- a/lib/src/message_widget.dart +++ b/lib/src/message_widget.dart @@ -394,7 +394,7 @@ class _MessageWidgetState extends State { showUsername || showTimeStamp || showInChannel) - _buildBottomRows(leftPadding) + _buildBottomRow(leftPadding) ], ), ], @@ -405,7 +405,7 @@ class _MessageWidgetState extends State { ); } - Widget _buildBottomRows(double leftPadding) { + Widget _buildBottomRow(double leftPadding) { final deleted = widget.message.isDeleted; var children = []; if (deleted) { From 538029a5de21f168820de12edd4bc754d5be19c8 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Wed, 23 Dec 2020 14:48:23 +0530 Subject: [PATCH 06/10] [Stream Channel] Fix getReplies Signed-off-by: Sahil Kumar --- lib/src/stream_channel.dart | 59 +++++++++++++++++++++++++++++-------- 1 file changed, 47 insertions(+), 12 deletions(-) diff --git a/lib/src/stream_channel.dart b/lib/src/stream_channel.dart index d5a4b030..be6cb4a5 100644 --- a/lib/src/stream_channel.dart +++ b/lib/src/stream_channel.dart @@ -134,6 +134,40 @@ class StreamChannelState extends State { return _queryBottomMessages(); } + //if (_queryMessageController.value == true || _paginationEnded) { + // return; + // } + // + // _queryMessageController.add(true); + // + // String firstId; + // if (widget.channel.state.threads.containsKey(parentId)) { + // final thread = widget.channel.state.threads[parentId]; + // + // if (thread != null && thread.isNotEmpty) { + // firstId = thread?.first?.id; + // } + // } + // + // final messageLimit = 50; + // return widget.channel + // .getReplies( + // parentId, + // PaginationParams( + // lessThan: firstId, + // limit: messageLimit, + // ), + // preferOffline: true, + // ) + // .then((res) { + // if (res.messages.isEmpty || res.messages.length < messageLimit) { + // _paginationEnded = true; + // } + // _queryMessageController.add(false); + // }).catchError((e, stack) { + // _queryMessageController.addError(e, stack); + // }); + /// Calls [channel.getReplies] updating [queryMessage] stream Future getReplies( String parentId, { @@ -143,23 +177,24 @@ class StreamChannelState extends State { if (_topPaginationEnded || _queryTopMessagesController.value) return; _queryTopMessagesController.add(true); - if (!channel.state.threads.containsKey(parentId)) { - return _queryTopMessagesController.add(false); + Message message; + if (channel.state.threads.containsKey(parentId)) { + final thread = channel.state.threads[parentId]; + if (thread.isNotEmpty) { + message = thread.first; + } } - final thread = channel.state.threads[parentId]; - - if (thread.isEmpty) return _queryTopMessagesController.add(false); - - final message = thread.first; - try { - final state = await queryBeforeMessage( - message.id, - limit: limit, + final response = await channel.getReplies( + parentId, + PaginationParams( + lessThan: message?.id, + limit: limit, + ), preferOffline: preferOffline, ); - if (state.messages.isEmpty || state.messages.length < limit) { + if (response.messages.isEmpty || response.messages.length < limit) { _topPaginationEnded = true; } _queryTopMessagesController.add(false); From 6b97879009eed22aa42474ced2f423cf3f4d8459 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Wed, 23 Dec 2020 14:53:34 +0530 Subject: [PATCH 07/10] cleanup Signed-off-by: Sahil Kumar --- lib/src/stream_channel.dart | 34 ---------------------------------- 1 file changed, 34 deletions(-) diff --git a/lib/src/stream_channel.dart b/lib/src/stream_channel.dart index be6cb4a5..8b4b97ef 100644 --- a/lib/src/stream_channel.dart +++ b/lib/src/stream_channel.dart @@ -134,40 +134,6 @@ class StreamChannelState extends State { return _queryBottomMessages(); } - //if (_queryMessageController.value == true || _paginationEnded) { - // return; - // } - // - // _queryMessageController.add(true); - // - // String firstId; - // if (widget.channel.state.threads.containsKey(parentId)) { - // final thread = widget.channel.state.threads[parentId]; - // - // if (thread != null && thread.isNotEmpty) { - // firstId = thread?.first?.id; - // } - // } - // - // final messageLimit = 50; - // return widget.channel - // .getReplies( - // parentId, - // PaginationParams( - // lessThan: firstId, - // limit: messageLimit, - // ), - // preferOffline: true, - // ) - // .then((res) { - // if (res.messages.isEmpty || res.messages.length < messageLimit) { - // _paginationEnded = true; - // } - // _queryMessageController.add(false); - // }).catchError((e, stack) { - // _queryMessageController.addError(e, stack); - // }); - /// Calls [channel.getReplies] updating [queryMessage] stream Future getReplies( String parentId, { From beea7e1c673686398fe4f1b31078a4b05be12073 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Wed, 23 Dec 2020 14:57:03 +0530 Subject: [PATCH 08/10] [Message Actions Modal] Hide showInChannel Indicator Signed-off-by: Sahil Kumar --- lib/src/message_actions_modal.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/src/message_actions_modal.dart b/lib/src/message_actions_modal.dart index f4cd47c8..8013a8b3 100644 --- a/lib/src/message_actions_modal.dart +++ b/lib/src/message_actions_modal.dart @@ -118,6 +118,7 @@ class MessageActionsModal extends StatelessWidget { showTimestamp: false, translateUserAvatar: false, showReactionPickerIndicator: true, + showInChannelIndicator: false, showSendingIndicator: DisplayWidget.gone, shape: messageShape, ), From 7efb6ca3dd25d07d12ba2f09e12dae36796840f1 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Wed, 23 Dec 2020 15:13:17 +0530 Subject: [PATCH 09/10] [MessageListView] Change parentMessage gradient alignment Signed-off-by: Sahil Kumar --- lib/src/message_list_view.dart | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/src/message_list_view.dart b/lib/src/message_list_view.dart index 24d32d88..a856db2b 100644 --- a/lib/src/message_list_view.dart +++ b/lib/src/message_list_view.dart @@ -339,6 +339,8 @@ class _MessageListViewState extends State { Container( decoration: BoxDecoration( gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, colors: [ Color(0XFFF7F7F7), Color(0XFFFCFCFC), From bb490849521ecaeba839486d883b938f2efc4b1b Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Wed, 23 Dec 2020 15:30:41 +0530 Subject: [PATCH 10/10] Hide timeStamp and sendingIndicator for a threaded reply message Signed-off-by: Sahil Kumar --- lib/src/message_widget.dart | 38 +++++++++++++++++++++---------------- 1 file changed, 22 insertions(+), 16 deletions(-) diff --git a/lib/src/message_widget.dart b/lib/src/message_widget.dart index c52e7f63..9d81d679 100644 --- a/lib/src/message_widget.dart +++ b/lib/src/message_widget.dart @@ -428,23 +428,11 @@ class _MessageWidgetState extends State { ], ), ); - } else { - final showSendingIndicator = - widget.showSendingIndicator == DisplayWidget.show; - final replyCount = widget.message.replyCount; - final msg = showInChannel - ? 'Thread Reply' - : replyCount != 0 - ? '$replyCount ${replyCount > 1 ? 'Thread Replies' : 'Thread Reply'}' - : 'Thread Reply'; - + } else if (showInChannel) { final onThreadTap = () async { try { - var message = widget.message; - if (showInChannel && message.parentId != null) { - final channel = StreamChannel.of(context); - message = await channel.getMessage(widget.message.parentId); - } + final channel = StreamChannel.of(context); + final message = await channel.getMessage(widget.message.parentId); return widget.onThreadTap(message); } catch (e, stk) { print(e); @@ -452,6 +440,24 @@ class _MessageWidgetState extends State { return null; } }; + children.add( + InkWell( + onTap: widget.onThreadTap != null ? onThreadTap : null, + child: Text('Thread Reply', style: widget.messageTheme?.replies), + ), + ); + } else { + final showSendingIndicator = + widget.showSendingIndicator == DisplayWidget.show; + final replyCount = widget.message.replyCount; + final msg = replyCount != 0 + ? '$replyCount ${replyCount > 1 ? 'Thread Replies' : 'Thread Reply'}' + : 'Thread Reply'; + + final onThreadTap = () async { + var message = widget.message; + return widget.onThreadTap(message); + }; children.addAll([ if (showSendingIndicator) _buildSendingIndicator(), @@ -463,7 +469,7 @@ class _MessageWidgetState extends State { child: _buildReadIndicator(), ), ), - if (showThreadReplyIndicator || showInChannel) + if (showThreadReplyIndicator) InkWell( onTap: widget.onThreadTap != null ? onThreadTap : null, child: Text(msg, style: widget.messageTheme?.replies),