From bc1761867d73a7fd73c1f326b94e4465ea4e4e1c Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Tue, 12 Jan 2021 12:27:42 +0530 Subject: [PATCH 01/28] [MessageListView] Make thread type messages all corners rounded Signed-off-by: Sahil Kumar --- lib/src/message_list_view.dart | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/lib/src/message_list_view.dart b/lib/src/message_list_view.dart index cda0d64c..2e45dff7 100644 --- a/lib/src/message_list_view.dart +++ b/lib/src/message_list_view.dart @@ -857,14 +857,21 @@ class _MessageListViewState extends State { attachmentBorderRadiusGeometry: BorderRadius.only( topLeft: Radius.circular(attachmentBorderRadius), bottomLeft: Radius.circular( - timeDiff >= 1 || !isNextUserSame ? 0 : attachmentBorderRadius), + (timeDiff >= 1 || !isNextUserSame) && !hasReplies && !isThreadMessage + ? 0 + : attachmentBorderRadius, + ), topRight: Radius.circular(attachmentBorderRadius), bottomRight: Radius.circular(attachmentBorderRadius), ), attachmentPadding: const EdgeInsets.all(2), borderRadiusGeometry: BorderRadius.only( topLeft: Radius.circular(16), - bottomLeft: Radius.circular(timeDiff >= 1 || !isNextUserSame ? 0 : 16), + bottomLeft: Radius.circular( + (timeDiff >= 1 || !isNextUserSame) && !hasReplies && !isThreadMessage + ? 0 + : 16, + ), topRight: Radius.circular(16), bottomRight: Radius.circular(16), ), From 4a7350530ebb2ba4a0aae634f484ef34db37172e Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Tue, 12 Jan 2021 12:52:52 +0530 Subject: [PATCH 02/28] [MessageListView] Remove topMessage extra top padding in thread conversation. Signed-off-by: Sahil Kumar --- lib/src/message_list_view.dart | 107 ++++++++++++++++----------------- 1 file changed, 52 insertions(+), 55 deletions(-) diff --git a/lib/src/message_list_view.dart b/lib/src/message_list_view.dart index 2e45dff7..aa557c6a 100644 --- a/lib/src/message_list_view.dart +++ b/lib/src/message_list_view.dart @@ -338,9 +338,28 @@ class _MessageListViewState extends State { messages.length + 2 + (_isThreadConversation ? 1 : 0), separatorBuilder: (context, i) { if (i == messages.length) return Offstage(); - if (i == messages.length + 2) return Offstage(); - if (i == messages.length + 1) 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( @@ -385,30 +404,7 @@ class _MessageListViewState extends State { widget.parentMessage, ); } else { - return Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - buildParentMessage(widget.parentMessage), - Container( - decoration: BoxDecoration( - gradient: StreamChatTheme.of(context) - .colorTheme - .bgGradient, - ), - child: Padding( - padding: const EdgeInsets.all(8.0), - child: Text( - '${widget.parentMessage.replyCount} ${widget.parentMessage.replyCount == 1 ? 'Reply' : 'Replies'}', - textAlign: TextAlign.center, - style: StreamChatTheme.of(context) - .channelTheme - .channelHeaderTheme - .lastMessageAt, - ), - ), - ), - ], - ); + return buildParentMessage(widget.parentMessage); } } if (i == messages.length + 1) { @@ -602,37 +598,38 @@ class _MessageListViewState extends State { ? streamChannel.queryTopMessages : streamChannel.queryBottomMessages; return StreamBuilder( - key: Key('LOADING-INDICATOR'), - stream: stream, - initialData: false, - builder: (context, snapshot) { - if (snapshot.hasError) { - return Container( - color: StreamChatTheme.of(context) - .colorTheme - .accentRed - .withOpacity(.2), - child: Center( - child: Text('Error loading messages'), - ), - ); - } - if (!snapshot.data) { - if (direction == QueryDirection.top) { - return Container( - height: 52, - width: double.infinity, - ); - } - return Offstage(); - } - return Center( - child: Padding( - padding: const EdgeInsets.all(8.0), - child: const CircularProgressIndicator(), + key: Key('LOADING-INDICATOR'), + stream: stream, + initialData: false, + builder: (context, snapshot) { + if (snapshot.hasError) { + return Container( + color: StreamChatTheme.of(context) + .colorTheme + .accentRed + .withOpacity(.2), + child: Center( + child: Text('Error loading messages'), ), ); - }); + } + if (!snapshot.data) { + if (!_isThreadConversation && direction == QueryDirection.top) { + return Container( + height: 52, + width: double.infinity, + ); + } + return Offstage(); + } + return Center( + child: Padding( + padding: const EdgeInsets.all(8.0), + child: const CircularProgressIndicator(), + ), + ); + }, + ); } Widget _buildTopMessage( From 9379c4672701689e31a2c36c1a091f740a37a921 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Tue, 12 Jan 2021 14:32:02 +0530 Subject: [PATCH 03/28] [QuotedMessageWidget] Fix attachment getting cropped by border side Signed-off-by: Sahil Kumar --- lib/src/quoted_message_widget.dart | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/lib/src/quoted_message_widget.dart b/lib/src/quoted_message_widget.dart index 7621174f..d012e15a 100644 --- a/lib/src/quoted_message_widget.dart +++ b/lib/src/quoted_message_widget.dart @@ -235,9 +235,7 @@ class QuotedMessageWidget extends StatelessWidget { ShapeBorder _getDefaultShape(BuildContext context) { return RoundedRectangleBorder( - side: BorderSide( - color: StreamChatTheme.of(context).colorTheme.greyWhisper, - ), + side: BorderSide(width: 0.0, color: Colors.transparent), borderRadius: BorderRadius.circular(8), ); } From f9ac3fe6faf6d4d2c58b9e293b3684b58399cd04 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Tue, 12 Jan 2021 14:58:43 +0530 Subject: [PATCH 04/28] [MessageListView] minor fixes Signed-off-by: Sahil Kumar --- lib/src/message_list_view.dart | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/lib/src/message_list_view.dart b/lib/src/message_list_view.dart index aa557c6a..ba9c499e 100644 --- a/lib/src/message_list_view.dart +++ b/lib/src/message_list_view.dart @@ -708,6 +708,7 @@ class _MessageListViewState extends State { Message message, ) { final isMyMessage = message.user.id == StreamChat.of(context).user.id; + final isOnlyEmoji = textIsOnlyEmoji(message.text); return MessageWidget( showThreadReplyIndicator: false, @@ -721,12 +722,7 @@ class _MessageListViewState extends State { message: message, reverse: isMyMessage, showUsername: !isMyMessage, - padding: EdgeInsets.only( - top: 8.0, - left: 8.0, - right: 8.0, - bottom: 16.0, - ), + padding: const EdgeInsets.all(8.0), showSendingIndicator: false, onThreadTap: _onThreadTap, borderRadiusGeometry: BorderRadius.only( @@ -735,7 +731,7 @@ class _MessageListViewState extends State { topRight: Radius.circular(16), bottomRight: Radius.circular(16), ), - borderSide: isMyMessage ? BorderSide.none : null, + borderSide: isMyMessage && !isOnlyEmoji ? BorderSide.none : null, showUserAvatar: isMyMessage ? DisplayWidget.gone : DisplayWidget.show, messageTheme: isMyMessage ? StreamChatTheme.of(context).ownMessageTheme @@ -810,8 +806,9 @@ class _MessageListViewState extends State { final showSendingIndicator = isMyMessage && (index == 0 || timeDiff >= 1 || !isNextUserSame); - bool showInChannelIndicator = !_isThreadConversation && isThreadMessage; - bool showThreadReplyIndicator = !_isThreadConversation && hasReplies; + final showInChannelIndicator = !_isThreadConversation && isThreadMessage; + final showThreadReplyIndicator = !_isThreadConversation && hasReplies; + final isOnlyEmoji = textIsOnlyEmoji(message.text); Widget child = MessageWidget( key: ValueKey('MESSAGE-${message.id}'), @@ -848,7 +845,7 @@ class _MessageListViewState extends State { showEditMessage: isMyMessage, showDeleteMessage: isMyMessage, showThreadReplyMessage: !isThreadMessage, - borderSide: isMyMessage ? BorderSide.none : null, + borderSide: isMyMessage && !isOnlyEmoji ? BorderSide.none : null, onThreadTap: _onThreadTap, onReplyTap: widget.onReplyTap, attachmentBorderRadiusGeometry: BorderRadius.only( From 85aaf9eab89bc1dd1c1191166d370fc18fdaa3d9 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Tue, 12 Jan 2021 15:05:50 +0530 Subject: [PATCH 05/28] [MessageListView] simplify some conditions Signed-off-by: Sahil Kumar --- lib/src/message_list_view.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/src/message_list_view.dart b/lib/src/message_list_view.dart index ba9c499e..6f61e261 100644 --- a/lib/src/message_list_view.dart +++ b/lib/src/message_list_view.dart @@ -851,7 +851,7 @@ class _MessageListViewState extends State { attachmentBorderRadiusGeometry: BorderRadius.only( topLeft: Radius.circular(attachmentBorderRadius), bottomLeft: Radius.circular( - (timeDiff >= 1 || !isNextUserSame) && !hasReplies && !isThreadMessage + (timeDiff >= 1 || !isNextUserSame) && !(hasReplies || isThreadMessage) ? 0 : attachmentBorderRadius, ), @@ -862,7 +862,7 @@ class _MessageListViewState extends State { borderRadiusGeometry: BorderRadius.only( topLeft: Radius.circular(16), bottomLeft: Radius.circular( - (timeDiff >= 1 || !isNextUserSame) && !hasReplies && !isThreadMessage + (timeDiff >= 1 || !isNextUserSame) && !(hasReplies || isThreadMessage) ? 0 : 16, ), From 0605cbc3b4ffcfd68464c8be3afb812922a1431f Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Tue, 12 Jan 2021 15:30:45 +0530 Subject: [PATCH 06/28] Move `isOnlyEmoji` utility function into String extension Signed-off-by: Sahil Kumar --- lib/src/extension.dart | 21 +++++++++++++++++++++ lib/src/message_list_view.dart | 5 +++-- lib/src/message_widget.dart | 2 +- lib/src/quoted_message_widget.dart | 2 +- lib/src/utils.dart | 14 -------------- 5 files changed, 26 insertions(+), 18 deletions(-) diff --git a/lib/src/extension.dart b/lib/src/extension.dart index 71edefe6..3ffd0e24 100644 --- a/lib/src/extension.dart +++ b/lib/src/extension.dart @@ -1,7 +1,28 @@ +import 'package:emojis/emoji.dart'; +import 'package:characters/characters.dart'; + +final _emojis = Emoji.all(); + extension StringExtension on String { String capitalize() { return "${this[0].toUpperCase()}${this.substring(1)}"; } + + // Emojis guidelines + // 1 to 3 emojis: big size with no text bubble. + // 4+ emojis or emojis+text: standard size with text bubble. + bool get isOnlyEmoji { + final characters = this.trim().characters; + if (characters.isEmpty) return false; + if (characters.length > 3) return false; + return characters.every((c) { + return _emojis.firstWhere( + (Emoji emoji) => emoji.char.contains(c), + orElse: () => null, + ) != + null; + }); + } } /// List extension diff --git a/lib/src/message_list_view.dart b/lib/src/message_list_view.dart index 6f61e261..095b6b73 100644 --- a/lib/src/message_list_view.dart +++ b/lib/src/message_list_view.dart @@ -17,6 +17,7 @@ import '../stream_chat_flutter.dart'; import 'date_divider.dart'; import 'stream_channel.dart'; import 'swipeable.dart'; +import 'extension.dart'; typedef MessageBuilder = Widget Function( BuildContext, @@ -708,7 +709,7 @@ class _MessageListViewState extends State { Message message, ) { final isMyMessage = message.user.id == StreamChat.of(context).user.id; - final isOnlyEmoji = textIsOnlyEmoji(message.text); + final isOnlyEmoji = message.text.isOnlyEmoji; return MessageWidget( showThreadReplyIndicator: false, @@ -808,7 +809,7 @@ class _MessageListViewState extends State { final showInChannelIndicator = !_isThreadConversation && isThreadMessage; final showThreadReplyIndicator = !_isThreadConversation && hasReplies; - final isOnlyEmoji = textIsOnlyEmoji(message.text); + final isOnlyEmoji = message.text.isOnlyEmoji; Widget child = MessageWidget( key: ValueKey('MESSAGE-${message.id}'), diff --git a/lib/src/message_widget.dart b/lib/src/message_widget.dart index e9ea4873..cd14ac40 100644 --- a/lib/src/message_widget.dart +++ b/lib/src/message_widget.dart @@ -938,7 +938,7 @@ class _MessageWidgetState extends State { ); } - bool get isOnlyEmoji => textIsOnlyEmoji(widget.message.text); + bool get isOnlyEmoji => widget.message.text.isOnlyEmoji; Color _getBackgroundColor() { if (hasQuotedMessage) { diff --git a/lib/src/quoted_message_widget.dart b/lib/src/quoted_message_widget.dart index d012e15a..8a102635 100644 --- a/lib/src/quoted_message_widget.dart +++ b/lib/src/quoted_message_widget.dart @@ -125,7 +125,7 @@ class QuotedMessageWidget extends StatelessWidget { } Widget _buildMessage(BuildContext context) { - final isOnlyEmoji = textIsOnlyEmoji(message.text); + final isOnlyEmoji = message.text.isOnlyEmoji; var msg = _hasAttachments && !_containsText ? message.copyWith(text: message.attachments.last?.title ?? '') : message; diff --git a/lib/src/utils.dart b/lib/src/utils.dart index be10263e..333b96f1 100644 --- a/lib/src/utils.dart +++ b/lib/src/utils.dart @@ -1,4 +1,3 @@ -import 'package:emojis/emoji.dart'; import 'package:flutter/material.dart'; import 'package:stream_chat/stream_chat.dart'; import 'package:url_launcher/url_launcher.dart'; @@ -215,16 +214,3 @@ StreamSvgIcon getFileTypeImage(String type) { break; } } - -final _emojis = Emoji.all(); - -bool textIsOnlyEmoji(String text) { - return text.trim().characters.isNotEmpty && - text.trim().characters.every((c) => - _emojis.firstWhere( - (Emoji emoji) => emoji.char.contains(c), - orElse: () => null, - ) != - null) && - text.characters.length < 4; -} From fbc8fd9a5937f5b0826cbb625183c6fa95091bc5 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Tue, 12 Jan 2021 15:38:54 +0530 Subject: [PATCH 07/28] [QuotedMessageWidget, MessageWidget] Fix emoji and regular text size Signed-off-by: Sahil Kumar --- lib/src/message_widget.dart | 2 +- lib/src/quoted_message_widget.dart | 7 +++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/lib/src/message_widget.dart b/lib/src/message_widget.dart index cd14ac40..a40556a2 100644 --- a/lib/src/message_widget.dart +++ b/lib/src/message_widget.dart @@ -923,7 +923,7 @@ class _MessageWidgetState extends State { ? widget.messageTheme.copyWith( messageText: widget.messageTheme.messageText.copyWith( - fontSize: 40, + fontSize: 42, )) : widget.messageTheme, ), diff --git a/lib/src/quoted_message_widget.dart b/lib/src/quoted_message_widget.dart index 8a102635..c718cdb5 100644 --- a/lib/src/quoted_message_widget.dart +++ b/lib/src/quoted_message_widget.dart @@ -145,9 +145,12 @@ class QuotedMessageWidget extends StatelessWidget { messageTheme: isOnlyEmoji && _containsText ? messageTheme.copyWith( messageText: messageTheme.messageText.copyWith( - fontSize: 24, + fontSize: 32, )) - : messageTheme, + : messageTheme.copyWith( + messageText: messageTheme.messageText.copyWith( + fontSize: 12, + )), ), ), ), From d9a50ae42eee303eae2bdbfbb21559fa18ff8e09 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Tue, 12 Jan 2021 12:05:01 +0100 Subject: [PATCH 08/28] fix animations --- example/pubspec.yaml | 2 +- lib/src/message_actions_modal.dart | 283 +++++++++++++-------------- lib/src/message_reactions_modal.dart | 265 ++++++++++++------------- lib/src/reaction_picker.dart | 30 +-- 4 files changed, 284 insertions(+), 296 deletions(-) diff --git a/example/pubspec.yaml b/example/pubspec.yaml index cdc07de7..46dc8820 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -1,6 +1,6 @@ name: example description: A new Flutter project. -version: 1.1.0+1 +version: 1.1.0+2 environment: sdk: ">=2.2.2 <3.0.0" diff --git a/lib/src/message_actions_modal.dart b/lib/src/message_actions_modal.dart index 95ecdea5..7f9c5e85 100644 --- a/lib/src/message_actions_modal.dart +++ b/lib/src/message_actions_modal.dart @@ -84,155 +84,146 @@ class _MessageActionsModalState extends State { ? 1 : (roughSentenceSize == 0 ? 1 : (roughSentenceSize / roughMaxSize)); - return GestureDetector( - behavior: HitTestBehavior.translucent, - onTap: () => Navigator.maybePop(context), - child: Stack( - children: [ - Positioned.fill( - child: BackdropFilter( - filter: ImageFilter.blur( - sigmaX: 10, - sigmaY: 10, - ), - child: Container( - color: StreamChatTheme.of(context).colorTheme.overlay, - ), - ), - ), - Center( - child: SingleChildScrollView( - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 8.0), - child: Column( - crossAxisAlignment: widget.reverse - ? CrossAxisAlignment.end - : CrossAxisAlignment.start, - children: [ - if (widget.showReactions && - (widget.message.status == MessageSendingStatus.SENT || - widget.message.status == null)) - Align( - alignment: Alignment( - user.id == widget.message.user.id - ? (divFactor > 1.0 ? 0.0 : (1.0 - divFactor)) - : (divFactor > 1.0 ? 0.0 : -(1.0 - divFactor)), - 0.0), - child: ReactionPicker( - message: widget.message, - messageTheme: widget.messageTheme, - ), - ), - TweenAnimationBuilder( - tween: Tween(begin: 0.0, end: 1.0), - duration: Duration(milliseconds: 300), - builder: (context, val, snapshot) { - return Transform.scale( - scale: val, - child: IgnorePointer( - child: MessageWidget( - key: Key('MessageWidget'), - reverse: widget.reverse, - message: widget.message.copyWith( - text: widget.message.text.length > 200 - ? '${widget.message.text.substring(0, 200)}...' - : widget.message.text, - ), - messageTheme: widget.messageTheme, - showReactions: false, - showUsername: false, - showThreadReplyIndicator: false, - showReplyMessage: false, - showUserAvatar: widget.showUserAvatar, - showTimestamp: false, - translateUserAvatar: false, - showReactionPickerIndicator: - widget.showReactions && - (widget.message.status == - MessageSendingStatus.SENT || - widget.message.status == null), - showInChannelIndicator: false, - showSendingIndicator: false, - shape: widget.messageShape, - ), - ), - ); - }), - TweenAnimationBuilder( - tween: Tween(begin: 0.0, end: 1.0), - duration: Duration(milliseconds: 300), - curve: Curves.easeInOut, - builder: (context, val, wid) { - return Transform( - transform: Matrix4.identity() - ..scale(val) - ..rotateZ(-1.0 + val), - alignment: widget.reverse - ? Alignment.topRight - : Alignment.topLeft, - child: Padding( - padding: EdgeInsets.only( - right: widget.reverse ? 16 : 0, - left: widget.reverse ? 0 : 48, - ), - child: SizedBox( - width: MediaQuery.of(context).size.width * 0.75, - child: Material( - color: StreamChatTheme.of(context) - .colorTheme - .whiteSnow, - clipBehavior: Clip.hardEdge, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(16), - ), - 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(), - ), - ), - ), - ), - ); - }) - ], + return TweenAnimationBuilder( + tween: Tween(begin: 0.0, end: 1.0), + duration: Duration(milliseconds: 300), + curve: Curves.easeInOutBack, + builder: (context, val, snapshot) { + return GestureDetector( + behavior: HitTestBehavior.translucent, + onTap: () => Navigator.maybePop(context), + child: Stack( + children: [ + Positioned.fill( + child: BackdropFilter( + filter: ImageFilter.blur( + sigmaX: 10, + sigmaY: 10, + ), + child: Container( + color: StreamChatTheme.of(context).colorTheme.overlay, + ), ), ), - ), + Transform.scale( + scale: val, + child: Center( + child: SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0), + child: Column( + crossAxisAlignment: widget.reverse + ? CrossAxisAlignment.end + : CrossAxisAlignment.start, + children: [ + if (widget.showReactions && + (widget.message.status == + MessageSendingStatus.SENT || + widget.message.status == null)) + Align( + alignment: Alignment( + user.id == widget.message.user.id + ? (divFactor > 1.0 + ? 0.0 + : (1.0 - divFactor)) + : (divFactor > 1.0 + ? 0.0 + : -(1.0 - divFactor)), + 0.0), + child: ReactionPicker( + message: widget.message, + messageTheme: widget.messageTheme, + ), + ), + IgnorePointer( + child: MessageWidget( + key: Key('MessageWidget'), + reverse: widget.reverse, + message: widget.message.copyWith( + text: widget.message.text.length > 200 + ? '${widget.message.text.substring(0, 200)}...' + : widget.message.text, + ), + messageTheme: widget.messageTheme, + showReactions: false, + showUsername: false, + showThreadReplyIndicator: false, + showReplyMessage: false, + showUserAvatar: widget.showUserAvatar, + showTimestamp: false, + translateUserAvatar: false, + showReactionPickerIndicator: + widget.showReactions && + (widget.message.status == + MessageSendingStatus.SENT || + widget.message.status == null), + showInChannelIndicator: false, + showSendingIndicator: false, + shape: widget.messageShape, + ), + ), + Padding( + padding: EdgeInsets.only( + right: widget.reverse ? 16 : 0, + left: widget.reverse ? 0 : 48, + ), + child: SizedBox( + width: MediaQuery.of(context).size.width * 0.75, + child: Material( + color: StreamChatTheme.of(context) + .colorTheme + .whiteSnow, + clipBehavior: Clip.hardEdge, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), + 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(), + ), + ), + ), + ), + ], + ), + ), + ), + ), + ), + ], ), - ], - ), + ); + }, ); } diff --git a/lib/src/message_reactions_modal.dart b/lib/src/message_reactions_modal.dart index 5c24e047..15a301cc 100644 --- a/lib/src/message_reactions_modal.dart +++ b/lib/src/message_reactions_modal.dart @@ -56,86 +56,92 @@ class MessageReactionsModal extends StatelessWidget { ? 1 : (roughSentenceSize == 0 ? 1 : (roughSentenceSize / roughMaxSize)); - return GestureDetector( - behavior: HitTestBehavior.translucent, - onTap: () => Navigator.maybePop(context), - child: Stack( - children: [ - Positioned.fill( - child: BackdropFilter( - filter: ImageFilter.blur( - sigmaX: 10, - sigmaY: 10, - ), - child: Container( - color: StreamChatTheme.of(context).colorTheme.overlay, - ), - ), - ), - Center( - child: SingleChildScrollView( - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 8.0), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - if (showReactions && - (message.status == MessageSendingStatus.SENT || - message.status == null)) - Align( - alignment: Alignment( - user.id == message.user.id - ? (divFactor > 1.0 ? 0.0 : (1.0 - divFactor)) - : (divFactor > 1.0 ? 0.0 : -(1.0 - divFactor)), - 0.0), - child: ReactionPicker( - message: message, - messageTheme: messageTheme, - ), - ), - TweenAnimationBuilder( - tween: Tween(begin: 0.0, end: 1.0), - duration: Duration(milliseconds: 300), - builder: (context, val, snapshot) { - return Transform.scale( - scale: val, - child: IgnorePointer( - child: MessageWidget( - key: Key('MessageWidget'), - reverse: reverse, - message: message.copyWith( - text: message.text.length > 200 - ? '${message.text.substring(0, 200)}...' - : message.text, - ), - messageTheme: messageTheme, - showReactions: false, - showUsername: false, - showUserAvatar: showUserAvatar, - showThreadReplyIndicator: false, - showTimestamp: false, - translateUserAvatar: false, - showSendingIndicator: false, - shape: messageShape, - showInChannelIndicator: false, - showReactionPickerIndicator: showReactions && - (message.status == - MessageSendingStatus.SENT || - message.status == null), - ), - ), - ); - }), - if (message.latestReactions?.isNotEmpty == true) - _buildReactionCard(context), - ], + return TweenAnimationBuilder( + tween: Tween(begin: 0.0, end: 1.0), + duration: Duration(milliseconds: 300), + curve: Curves.easeInOutBack, + builder: (context, val, snapshot) { + return GestureDetector( + behavior: HitTestBehavior.translucent, + onTap: () => Navigator.maybePop(context), + child: Stack( + children: [ + Positioned.fill( + child: BackdropFilter( + filter: ImageFilter.blur( + sigmaX: 10, + sigmaY: 10, + ), + child: Container( + color: StreamChatTheme.of(context).colorTheme.overlay, + ), ), ), - ), + Transform.scale( + scale: val, + child: Center( + child: SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + if (showReactions && + (message.status == MessageSendingStatus.SENT || + message.status == null)) + Align( + alignment: Alignment( + user.id == message.user.id + ? (divFactor > 1.0 + ? 0.0 + : (1.0 - divFactor)) + : (divFactor > 1.0 + ? 0.0 + : -(1.0 - divFactor)), + 0.0), + child: ReactionPicker( + message: message, + messageTheme: messageTheme, + ), + ), + IgnorePointer( + child: MessageWidget( + key: Key('MessageWidget'), + reverse: reverse, + message: message.copyWith( + text: message.text.length > 200 + ? '${message.text.substring(0, 200)}...' + : message.text, + ), + messageTheme: messageTheme, + showReactions: false, + showUsername: false, + showUserAvatar: showUserAvatar, + showThreadReplyIndicator: false, + showTimestamp: false, + translateUserAvatar: false, + showSendingIndicator: false, + shape: messageShape, + showInChannelIndicator: false, + showReactionPickerIndicator: showReactions && + (message.status == + MessageSendingStatus.SENT || + message.status == null), + ), + ), + if (message.latestReactions?.isNotEmpty == true) + _buildReactionCard(context), + ], + ), + ), + ), + ), + ), + ], ), - ], - ), + ); + }, ); } @@ -191,64 +197,53 @@ class MessageReactionsModal extends StatelessWidget { BuildContext context, ) { final isCurrentUser = reaction.user.id == currentUser.id; - return TweenAnimationBuilder( - tween: Tween(begin: 0.0, end: 1.0), - duration: Duration(milliseconds: 300), - curve: Curves.easeInOut, - builder: (context, val, snapshot) { - return Transform.scale( - scale: val, - child: ConstrainedBox( - constraints: BoxConstraints.loose(Size( - 64, - 98, - )), - child: Column( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Stack( - children: [ - UserAvatar( - onTap: onUserAvatarTap, - user: reaction.user, - constraints: BoxConstraints.tightFor( - height: 64, - width: 64, - ), - borderRadius: BorderRadius.circular(32), - ), - Positioned( - child: Align( - alignment: reverse - ? Alignment.centerRight - : Alignment.centerLeft, - child: ReactionBubble( - reactions: [reaction], - flipTail: !reverse, - borderColor: messageTheme.reactionsBorderColor, - backgroundColor: - messageTheme.reactionsBackgroundColor, - highlightOwnReactions: false, - ), - ), - bottom: 6, - left: isCurrentUser ? 0 : null, - right: isCurrentUser ? 0 : null, - ), - ], - ), - const SizedBox(height: 8), - Text( - reaction.user.name, - style: StreamChatTheme.of(context).textTheme.footnoteBold, - textAlign: TextAlign.center, - ), - ], + return ConstrainedBox( + constraints: BoxConstraints.loose(Size( + 64, + 98, + )), + child: Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Stack( + children: [ + UserAvatar( + onTap: onUserAvatarTap, + user: reaction.user, + constraints: BoxConstraints.tightFor( + height: 64, + width: 64, + ), + borderRadius: BorderRadius.circular(32), ), - ), - ); - }); + Positioned( + child: Align( + alignment: + reverse ? Alignment.centerRight : Alignment.centerLeft, + child: ReactionBubble( + reactions: [reaction], + flipTail: !reverse, + borderColor: messageTheme.reactionsBorderColor, + backgroundColor: messageTheme.reactionsBackgroundColor, + highlightOwnReactions: false, + ), + ), + bottom: 6, + left: isCurrentUser ? 0 : null, + right: isCurrentUser ? 0 : null, + ), + ], + ), + const SizedBox(height: 8), + Text( + reaction.user.name, + style: StreamChatTheme.of(context).textTheme.footnoteBold, + textAlign: TextAlign.center, + ), + ], + ), + ); } } diff --git a/lib/src/reaction_picker.dart b/lib/src/reaction_picker.dart index c5c42bd1..bca74a99 100644 --- a/lib/src/reaction_picker.dart +++ b/lib/src/reaction_picker.dart @@ -1,3 +1,5 @@ +import 'dart:math'; + import 'package:ezanimation/ezanimation.dart'; import 'package:flutter/material.dart'; import 'package:stream_chat_flutter/src/stream_svg_icon.dart'; @@ -36,13 +38,10 @@ class _ReactionPickerState extends State if (animations.isEmpty && reactionIcons.isNotEmpty) { reactionIcons.forEach((element) { animations.add( - EzAnimation.sequence( - [ - SequenceItem(0.0, 1.4), - SequenceItem(1.4, 1.0), - ], + EzAnimation.tween( + Tween(begin: 0.0, end: 1.0), Duration(milliseconds: 500), - vsync: this, + curve: Curves.easeInOutBack, ), ); }); @@ -52,7 +51,7 @@ class _ReactionPickerState extends State return TweenAnimationBuilder( tween: Tween(begin: 0.0, end: 1.0), - curve: Curves.easeInOutExpo, + curve: Curves.easeInOutBack, duration: Duration(milliseconds: 500), builder: (context, val, wid) { return Transform.scale( @@ -81,15 +80,18 @@ class _ReactionPickerState extends State icon: AnimatedBuilder( animation: animations[index], builder: (context, val) { - return Transform( - transform: Matrix4.identity() - ..scale(animations[index].value, - animations[index].value) - ..rotateZ(1.0 - animations[index].value), + return Transform.scale( + scale: animations[index].value, child: StreamSvgIcon( assetName: reactionIcon.assetName, - height: animations[index].value * 24.0, - width: animations[index].value * 24.0, + height: max( + 0, + animations[index].value * 24.0, + ), + width: max( + 0, + animations[index].value * 24.0, + ), color: ownReactionIndex != -1 ? StreamChatTheme.of(context) .colorTheme From 47a3a94c93b38112c19612017f876d8383b34346 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Tue, 12 Jan 2021 16:57:04 +0530 Subject: [PATCH 09/28] [MessageInput] Fix iconButtons and textField paddings and text sizes Signed-off-by: Sahil Kumar --- lib/src/message_input.dart | 559 ++++++++++++++++++------------------- 1 file changed, 271 insertions(+), 288 deletions(-) diff --git a/lib/src/message_input.dart b/lib/src/message_input.dart index ee8763ba..8bb20302 100644 --- a/lib/src/message_input.dart +++ b/lib/src/message_input.dart @@ -358,31 +358,31 @@ class MessageInputState extends State { ); } - AnimatedCrossFade _animateSendButton(BuildContext context) { - return AnimatedCrossFade( - crossFadeState: ((_messageIsPresent || _attachments.isNotEmpty) && - _attachments.every((a) => a.uploaded == true)) - ? CrossFadeState.showFirst - : CrossFadeState.showSecond, - firstChild: _buildSendButton(context), - secondChild: _buildIdleSendButton(context), - duration: Duration(milliseconds: 300), - alignment: Alignment.center, + Widget _animateSendButton(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(8.0), + child: AnimatedCrossFade( + crossFadeState: ((_messageIsPresent || _attachments.isNotEmpty) && + _attachments.every((a) => a.uploaded == true)) + ? CrossFadeState.showFirst + : CrossFadeState.showSecond, + firstChild: _buildSendButton(context), + secondChild: _buildIdleSendButton(context), + duration: Duration(milliseconds: 300), + alignment: Alignment.center, + ), ); } Widget _buildExpandActionsButton() { - return AnimatedCrossFade( - crossFadeState: - _actionsShrunk ? CrossFadeState.showFirst : CrossFadeState.showSecond, - firstChild: Padding( - padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 8), - child: IconButton( - onPressed: () { - setState(() { - _actionsShrunk = false; - }); - }, + return Padding( + padding: const EdgeInsets.all(8.0), + child: AnimatedCrossFade( + crossFadeState: _actionsShrunk + ? CrossFadeState.showFirst + : CrossFadeState.showSecond, + firstChild: IconButton( + onPressed: () => setState(() => _actionsShrunk = false), icon: StreamSvgIcon.emptyCircleLeft( color: StreamChatTheme.of(context).colorTheme.accentBlue, ), @@ -393,34 +393,36 @@ class MessageInputState extends State { ), splashRadius: 24, ), + secondChild: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + if (!widget.disableAttachments) _buildAttachmentButton(), + if (widget.editMessage == null && + StreamChannel.of(context) + .channel + ?.config + ?.commands + ?.isNotEmpty == + true) + _buildCommandButton(), + ].insertBetween(const SizedBox(width: 8)), + ), + duration: Duration(milliseconds: 300), + alignment: Alignment.center, ), - secondChild: Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - if (!widget.disableAttachments) _buildAttachmentButton(), - if (widget.editMessage == null && - StreamChannel.of(context).channel?.config?.commands?.isNotEmpty == - true) - _buildCommandButton(), - ], - ), - duration: Duration(milliseconds: 300), - alignment: Alignment.center, ); } Expanded _buildTextInput(BuildContext context) { + final theme = StreamChatTheme.of(context); return Expanded( child: Center( child: Container( clipBehavior: Clip.antiAlias, decoration: BoxDecoration( - borderRadius: BorderRadius.circular(24.0), - border: Border.all( - color: StreamChatTheme.of(context).colorTheme.greyGainsboro, - ), + borderRadius: BorderRadius.circular(20.0), + border: Border.all(color: theme.colorTheme.greyGainsboro), ), - padding: _attachments.isEmpty ? null : EdgeInsets.all(6.0), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, @@ -429,52 +431,48 @@ class MessageInputState extends State { _buildAttachments(), LimitedBox( maxHeight: widget.maxHeight, - child: TextField( - key: Key('messageInputText'), - enabled: _inputEnabled, - minLines: null, - maxLines: null, - onSubmitted: (_) { - sendMessage(); - }, - keyboardType: widget.keyboardType, - controller: textEditingController, - focusNode: _focusNode, - style: Theme.of(context).textTheme.bodyText2, - autofocus: false, - textAlignVertical: TextAlignVertical.center, - decoration: InputDecoration( - isDense: true, - hintText: _getHint(), - prefixText: _commandEnabled ? null : ' ', - border: OutlineInputBorder( - borderSide: BorderSide(color: Colors.transparent)), - focusedBorder: OutlineInputBorder( - borderSide: BorderSide(color: Colors.transparent)), - enabledBorder: OutlineInputBorder( - borderSide: BorderSide(color: Colors.transparent)), - errorBorder: OutlineInputBorder( - borderSide: BorderSide(color: Colors.transparent)), - disabledBorder: OutlineInputBorder( - borderSide: BorderSide(color: Colors.transparent)), - contentPadding: EdgeInsets.symmetric( - horizontal: 16, - vertical: 13, - ), - prefixIcon: _commandEnabled - ? Padding( - padding: - const EdgeInsets.symmetric(horizontal: 8.0), - child: Chip( - backgroundColor: StreamChatTheme.of(context) - .colorTheme - .accentBlue, - padding: EdgeInsets.zero, - labelPadding: - EdgeInsets.symmetric(horizontal: 8.0), - label: Row( + child: SizedBox( + height: 40, + child: TextField( + key: Key('messageInputText'), + enabled: _inputEnabled, + minLines: null, + maxLines: null, + onSubmitted: (_) => sendMessage(), + keyboardType: widget.keyboardType, + controller: textEditingController, + focusNode: _focusNode, + style: theme.textTheme.body, + autofocus: false, + textAlignVertical: TextAlignVertical.center, + decoration: InputDecoration( + isDense: true, + hintText: _getHint(), + hintStyle: theme.textTheme.body.copyWith( + color: theme.colorTheme.grey, + ), + border: OutlineInputBorder( + borderSide: BorderSide(color: Colors.transparent)), + focusedBorder: OutlineInputBorder( + borderSide: BorderSide(color: Colors.transparent)), + enabledBorder: OutlineInputBorder( + borderSide: BorderSide(color: Colors.transparent)), + errorBorder: OutlineInputBorder( + borderSide: BorderSide(color: Colors.transparent)), + disabledBorder: OutlineInputBorder( + borderSide: BorderSide(color: Colors.transparent)), + contentPadding: const EdgeInsets.fromLTRB(16, 12, 13, 11), + prefixIcon: _commandEnabled + ? Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12), + color: theme.colorTheme.accentBlue, + ), + height: 24, + margin: const EdgeInsets.all(8.0), + padding: const EdgeInsets.only(right: 8, left: 4), + child: Row( mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.center, children: [ StreamSvgIcon.lightning( color: Colors.white, @@ -484,27 +482,32 @@ class MessageInputState extends State { _chosenCommand?.name?.toUpperCase() ?? '', style: StreamChatTheme.of(context) .textTheme - .footnote + .footnoteBold .copyWith( color: Colors.white, ), ), ], ), - ), - ) - : null, - suffixIcon: _commandEnabled - ? IconButton( - icon: StreamSvgIcon.close_small(), - splashRadius: 24, - onPressed: () { - setState(() => _commandEnabled = false); - }, - ) - : null, + ) + : null, + suffixIcon: _commandEnabled + ? IconButton( + icon: StreamSvgIcon.close_small(), + splashRadius: 24, + padding: const EdgeInsets.all(0), + constraints: BoxConstraints.tightFor( + height: 24, + width: 24, + ), + onPressed: () { + setState(() => _commandEnabled = false); + }, + ) + : null, + ), + textCapitalization: TextCapitalization.sentences, ), - textCapitalization: TextCapitalization.sentences, ), ) ], @@ -1515,9 +1518,7 @@ class MessageInputState extends State { } Widget _buildReplyToMessage() { - if (!_hasQuotedMessage) { - return Offstage(); - } + if (!_hasQuotedMessage) return Offstage(); final containsUrl = widget.quotedMessage.attachments ?.any((element) => element.ogScrapeUrl != null) == true; @@ -1534,109 +1535,104 @@ class MessageInputState extends State { } Widget _buildAttachments() { - return _attachments.isEmpty - ? Container() - : 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( - borderRadius: BorderRadius.circular(10), - clipBehavior: Clip.antiAlias, - child: FileAttachment( - attachment: e.attachment, - attachmentType: FileAttachmentType.local, - file: e.file, - size: Size( - MediaQuery.of(context).size.width * 0.65, - 56.0, + if (_attachments.isEmpty) return Offstage(); + 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( + borderRadius: BorderRadius.circular(10), + clipBehavior: Clip.antiAlias, + child: FileAttachment( + attachment: e.attachment, + attachmentType: FileAttachmentType.local, + file: e.file, + size: Size( + MediaQuery.of(context).size.width * 0.65, + 56.0, + ), + trailing: Padding( + padding: const EdgeInsets.all(8.0), + child: InkWell( + child: CircleAvatar( + backgroundColor: StreamChatTheme.of(context) + .colorTheme + .black + .withOpacity(0.6), + maxRadius: 12.0, + child: StreamSvgIcon.close( + color: StreamChatTheme.of(context) + .colorTheme + .white, ), - trailing: Padding( - padding: const EdgeInsets.all(8.0), - child: InkWell( - child: CircleAvatar( - backgroundColor: - StreamChatTheme.of(context) - .colorTheme - .black - .withOpacity(0.6), - maxRadius: 12.0, - child: StreamSvgIcon.close( - color: StreamChatTheme.of(context) - .colorTheme - .white, + ), + onTap: () { + setState(() { + _attachments.remove(e); + }); + }, + ), + ), + ), + ), + ), + ) + .toList(), + ), + ), + 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( + borderRadius: BorderRadius.circular(10), + clipBehavior: Clip.antiAlias, + child: Stack( + children: [ + AspectRatio( + aspectRatio: 1.0, + child: Container( + height: 104, + width: 104, + child: _buildAttachment(attachment), + ), + ), + _buildRemoveButton(attachment), + attachment.uploaded + ? SizedBox() + : Positioned.fill( + child: Center( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: CircularProgressIndicator(), ), ), - onTap: () { - setState(() { - _attachments.remove(e); - }); - }, ), - ), - ), - ), - ), - ) - .toList(), - ), - ), - 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( - borderRadius: BorderRadius.circular(10), - clipBehavior: Clip.antiAlias, - child: Stack( - children: [ - AspectRatio( - aspectRatio: 1.0, - child: Container( - height: 104, - width: 104, - child: _buildAttachment(attachment), - ), - ), - _buildRemoveButton(attachment), - attachment.uploaded - ? SizedBox() - : Positioned.fill( - child: Center( - child: Padding( - padding: - const EdgeInsets.all(16.0), - child: - CircularProgressIndicator(), - ), - ), - ), - ], - ), - ), - ), - ) - .toList(), - ), - ), - ], - ); + ], + ), + ), + ), + ) + .toList(), + ), + ), + ], + ); } Positioned _buildRemoveButton(_SendingAttachment attachment) { @@ -1746,80 +1742,74 @@ class MessageInputState extends State { } Widget _buildCommandButton() { - return Padding( - padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 8), - child: IconButton( - icon: StreamSvgIcon.lightning( - color: _commandsOverlay != null - ? StreamChatTheme.of(context).colorTheme.accentBlue - : StreamChatTheme.of(context).colorTheme.grey, - ), - padding: const EdgeInsets.all(0), - constraints: BoxConstraints.tightFor( - height: 24, - width: 24, - ), - splashRadius: 24, - onPressed: () async { - if (_openFilePickerSection) { - setState(() { - _animateContainer = false; - _openFilePickerSection = false; - _filePickerSize = _kMinMediaPickerSize; - }); - await Future.delayed(Duration(milliseconds: 300)); - } - - if (_commandsOverlay == null) { - setState(() { - _commandsOverlay = _buildCommandsOverlayEntry(); - Overlay.of(context).insert(_commandsOverlay); - }); - } else { - setState(() { - _commandsOverlay?.remove(); - _commandsOverlay = null; - }); - } - }, + return IconButton( + icon: StreamSvgIcon.lightning( + color: _commandsOverlay != null + ? StreamChatTheme.of(context).colorTheme.accentBlue + : StreamChatTheme.of(context).colorTheme.grey, ), + padding: const EdgeInsets.all(0), + constraints: BoxConstraints.tightFor( + height: 24, + width: 24, + ), + splashRadius: 24, + onPressed: () async { + if (_openFilePickerSection) { + setState(() { + _animateContainer = false; + _openFilePickerSection = false; + _filePickerSize = _kMinMediaPickerSize; + }); + await Future.delayed(Duration(milliseconds: 300)); + } + + if (_commandsOverlay == null) { + setState(() { + _commandsOverlay = _buildCommandsOverlayEntry(); + Overlay.of(context).insert(_commandsOverlay); + }); + } else { + setState(() { + _commandsOverlay?.remove(); + _commandsOverlay = null; + }); + } + }, ); } Widget _buildAttachmentButton() { - return Padding( - padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 8), - child: IconButton( - icon: StreamSvgIcon.attach( - color: _openFilePickerSection - ? StreamChatTheme.of(context).colorTheme.accentBlue - : StreamChatTheme.of(context).colorTheme.grey, - ), - padding: const EdgeInsets.all(0), - constraints: BoxConstraints.tightFor( - height: 24, - width: 24, - ), - splashRadius: 24, - onPressed: () async { - _emojiOverlay?.remove(); - _emojiOverlay = null; - _commandsOverlay?.remove(); - _commandsOverlay = null; - _mentionsOverlay?.remove(); - _mentionsOverlay = null; - - if (_openFilePickerSection) { - setState(() { - _animateContainer = true; - _openFilePickerSection = false; - _filePickerSize = _kMinMediaPickerSize; - }); - } else { - showAttachmentModal(); - } - }, + return IconButton( + icon: StreamSvgIcon.attach( + color: _openFilePickerSection + ? StreamChatTheme.of(context).colorTheme.accentBlue + : StreamChatTheme.of(context).colorTheme.grey, ), + padding: const EdgeInsets.all(0), + constraints: BoxConstraints.tightFor( + height: 24, + width: 24, + ), + splashRadius: 24, + onPressed: () async { + _emojiOverlay?.remove(); + _emojiOverlay = null; + _commandsOverlay?.remove(); + _commandsOverlay = null; + _mentionsOverlay?.remove(); + _mentionsOverlay = null; + + if (_openFilePickerSection) { + setState(() { + _animateContainer = true; + _openFilePickerSection = false; + _filePickerSize = _kMinMediaPickerSize; + }); + } else { + showAttachmentModal(); + } + }, ); } @@ -2118,31 +2108,24 @@ class MessageInputState extends State { } Widget _buildIdleSendButton(BuildContext context) { - return Padding( - padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 8), - child: StreamSvgIcon( - assetName: _getIdleSendIcon(), - color: StreamChatTheme.of(context).colorTheme.greyGainsboro, - ), + return StreamSvgIcon( + assetName: _getIdleSendIcon(), + color: StreamChatTheme.of(context).colorTheme.greyGainsboro, ); } Widget _buildSendButton(BuildContext context) { - return Padding( - padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 8), - child: IconButton( - onPressed: sendMessage, - visualDensity: VisualDensity.compact, - padding: const EdgeInsets.all(0), - splashRadius: 24, - constraints: BoxConstraints.tightFor( - height: 24, - width: 24, - ), - icon: StreamSvgIcon( - assetName: _getSendIcon(), - color: StreamChatTheme.of(context).colorTheme.accentBlue, - ), + return IconButton( + onPressed: sendMessage, + padding: const EdgeInsets.all(0), + splashRadius: 24, + constraints: BoxConstraints.tightFor( + height: 24, + width: 24, + ), + icon: StreamSvgIcon( + assetName: _getSendIcon(), + color: StreamChatTheme.of(context).colorTheme.accentBlue, ), ); } From c5f245808378e685af2daec504cc8989233706c2 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Tue, 12 Jan 2021 18:03:24 +0530 Subject: [PATCH 10/28] [MessageWidget] Move messageBorderSide handling one level up Signed-off-by: Sahil Kumar --- lib/src/message_list_view.dart | 10 +++++++++- lib/src/message_widget.dart | 7 ++----- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/lib/src/message_list_view.dart b/lib/src/message_list_view.dart index 095b6b73..dab8eda1 100644 --- a/lib/src/message_list_view.dart +++ b/lib/src/message_list_view.dart @@ -811,6 +811,14 @@ class _MessageListViewState extends State { final showThreadReplyIndicator = !_isThreadConversation && hasReplies; final isOnlyEmoji = message.text.isOnlyEmoji; + final borderSide = isMyMessage + ? !(showThreadReplyIndicator || showInChannelIndicator) + ? BorderSide.none + : null + : isOnlyEmoji + ? BorderSide.none + : null; + Widget child = MessageWidget( key: ValueKey('MESSAGE-${message.id}'), message: message, @@ -846,7 +854,7 @@ class _MessageListViewState extends State { showEditMessage: isMyMessage, showDeleteMessage: isMyMessage, showThreadReplyMessage: !isThreadMessage, - borderSide: isMyMessage && !isOnlyEmoji ? BorderSide.none : null, + borderSide: borderSide, onThreadTap: _onThreadTap, onReplyTap: widget.onReplyTap, attachmentBorderRadiusGeometry: BorderRadius.only( diff --git a/lib/src/message_widget.dart b/lib/src/message_widget.dart index a40556a2..ba458a7e 100644 --- a/lib/src/message_widget.dart +++ b/lib/src/message_widget.dart @@ -383,11 +383,8 @@ class _MessageWidgetState extends State { ), shape: widget.shape ?? RoundedRectangleBorder( - side: isOnlyEmoji && - !(showThreadReplyIndicator || - showInChannel) - ? BorderSide.none - : widget.borderSide ?? + side: + widget.borderSide ?? BorderSide( color: widget .messageTheme From f04452cbc9cfa27c8a6b8d37c47209b56fb0e8dd Mon Sep 17 00:00:00 2001 From: Deven Joshi Date: Tue, 12 Jan 2021 18:04:39 +0530 Subject: [PATCH 11/28] feat: Added info tile and wrapped channel list and channel header --- lib/src/channel_header.dart | 121 +++++++++++++++---------- lib/src/channel_list_header.dart | 147 ++++++++++++++++++------------- lib/src/info_tile.dart | 35 ++++++++ lib/src/stream_chat.dart | 35 ++++---- 4 files changed, 218 insertions(+), 120 deletions(-) create mode 100644 lib/src/info_tile.dart diff --git a/lib/src/channel_header.dart b/lib/src/channel_header.dart index 4517d7a0..fe624473 100644 --- a/lib/src/channel_header.dart +++ b/lib/src/channel_header.dart @@ -3,6 +3,7 @@ import 'package:stream_chat/stream_chat.dart'; import 'package:stream_chat_flutter/src/back_button.dart'; import 'package:stream_chat_flutter/src/channel_info.dart'; import 'package:stream_chat_flutter/src/channel_name.dart'; +import 'package:stream_chat_flutter/src/info_tile.dart'; import 'package:stream_chat_flutter/src/stream_chat_theme.dart'; import '../stream_chat_flutter.dart'; @@ -82,54 +83,84 @@ class ChannelHeader extends StatelessWidget implements PreferredSizeWidget { @override Widget build(BuildContext context) { final channel = StreamChannel.of(context).channel; - return AppBar( - brightness: Theme.of(context).brightness, - elevation: 1, - leading: showBackButton - ? StreamBackButton( - onPressed: onBackPressed, - showUnreads: true, - ) - : SizedBox(), - backgroundColor: - StreamChatTheme.of(context).channelTheme.channelHeaderTheme.color, - actions: [ - Padding( - padding: const EdgeInsets.only(right: 10.0), - child: Center( - child: ChannelImage( - onTap: onImageTap, - ), - ), - ), - ], - centerTitle: true, - title: InkWell( - onTap: onTitleTap, - child: Container( - height: preferredSize.height, - width: preferredSize.width, - child: Column( - crossAxisAlignment: CrossAxisAlignment.center, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - ChannelName( - textStyle: StreamChatTheme.of(context) - .channelTheme - .channelHeaderTheme - .title, - ), - SizedBox(height: 2), - ChannelInfo( - showTypingIndicator: showTypingIndicator, - channel: channel, - textStyle: - StreamChatTheme.of(context).channelPreviewTheme.subtitle, + final _client = StreamChat.of(context).client; + + return ValueListenableBuilder( + valueListenable: _client.wsConnectionStatus, + builder: (context, status, _) { + String statusString = ''; + bool showStatus = true; + + switch (status) { + case ConnectionStatus.connected: + statusString = 'Connected'; + showStatus = false; + break; + case ConnectionStatus.connecting: + statusString = 'Connecting'; + break; + case ConnectionStatus.disconnected: + statusString = 'Disconnected'; + break; + } + + return InfoTile( + showMessage: showStatus, + message: statusString, + child: AppBar( + brightness: Theme.of(context).brightness, + elevation: 1, + leading: showBackButton + ? StreamBackButton( + onPressed: onBackPressed, + showUnreads: true, + ) + : SizedBox(), + backgroundColor: StreamChatTheme.of(context) + .channelTheme + .channelHeaderTheme + .color, + actions: [ + Padding( + padding: const EdgeInsets.only(right: 10.0), + child: Center( + child: ChannelImage( + onTap: onImageTap, + ), + ), ), ], + centerTitle: true, + title: InkWell( + onTap: onTitleTap, + child: Container( + height: preferredSize.height, + width: preferredSize.width, + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + ChannelName( + textStyle: StreamChatTheme.of(context) + .channelTheme + .channelHeaderTheme + .title, + ), + SizedBox(height: 2), + ChannelInfo( + showTypingIndicator: showTypingIndicator, + channel: channel, + textStyle: StreamChatTheme.of(context) + .channelPreviewTheme + .subtitle, + ), + ], + ), + ), + ), ), - ), - ), + ); + }, ); } diff --git a/lib/src/channel_list_header.dart b/lib/src/channel_list_header.dart index 34f3cc76..e7fc9f9a 100644 --- a/lib/src/channel_list_header.dart +++ b/lib/src/channel_list_header.dart @@ -6,6 +6,7 @@ import 'package:stream_chat/stream_chat.dart'; import 'package:stream_chat_flutter/src/stream_neumorphic_button.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; +import 'info_tile.dart'; import 'stream_chat.dart'; typedef _TitleBuilder = Widget Function( @@ -72,73 +73,101 @@ class ChannelListHeader extends StatelessWidget implements PreferredSizeWidget { Widget build(BuildContext context) { final _client = client ?? StreamChat.of(context).client; final user = _client.state.user; - return AppBar( - brightness: Theme.of(context).brightness, - elevation: 1, - backgroundColor: - StreamChatTheme.of(context).channelTheme.channelHeaderTheme.color, - centerTitle: true, - leading: Center( - child: UserAvatar( - user: user, - showOnlineStatus: false, - onTap: onUserAvatarTap ?? (_) => Scaffold.of(context).openDrawer(), - borderRadius: BorderRadius.circular(20), - constraints: BoxConstraints.tightFor( - height: 40, - width: 40, - ), - ), - ), - actions: [ - StreamNeumorphicButton( - child: IconButton( - icon: ValueListenableBuilder( - valueListenable: _client.wsConnectionStatus, - builder: (context, status, child) { - var color; + return ValueListenableBuilder( + valueListenable: _client.wsConnectionStatus, + builder: (context, status, child) { + String statusString = ''; + bool showStatus = true; + + switch (status) { + case ConnectionStatus.connected: + statusString = 'Connected'; + showStatus = false; + break; + case ConnectionStatus.connecting: + statusString = 'Connecting'; + break; + case ConnectionStatus.disconnected: + statusString = 'Disconnected'; + break; + } + + return InfoTile( + showMessage: showStatus, + message: statusString, + child: AppBar( + brightness: Theme.of(context).brightness, + elevation: 1, + backgroundColor: StreamChatTheme.of(context) + .channelTheme + .channelHeaderTheme + .color, + centerTitle: true, + leading: Center( + child: UserAvatar( + user: user, + showOnlineStatus: false, + onTap: + onUserAvatarTap ?? (_) => Scaffold.of(context).openDrawer(), + borderRadius: BorderRadius.circular(20), + constraints: BoxConstraints.tightFor( + height: 40, + width: 40, + ), + ), + ), + actions: [ + StreamNeumorphicButton( + child: IconButton( + icon: ValueListenableBuilder( + valueListenable: _client.wsConnectionStatus, + builder: (context, status, child) { + var color; + switch (status) { + case ConnectionStatus.connected: + color = + StreamChatTheme.of(context).colorTheme.accentBlue; + break; + case ConnectionStatus.connecting: + color = Colors.grey; + break; + case ConnectionStatus.disconnected: + color = Colors.grey; + break; + } + return SvgPicture.asset( + 'svgs/icon_pen_write.svg', + package: 'stream_chat_flutter', + width: 24.0, + height: 24.0, + color: color, + ); + }, + ), + onPressed: onNewChatButtonTap, + ), + ) + ], + title: Builder( + builder: (context) { + if (titleBuilder != null) { + return titleBuilder(context, status, _client); + } switch (status) { case ConnectionStatus.connected: - color = StreamChatTheme.of(context).colorTheme.accentBlue; - break; + return _buildConnectedTitleState(context); case ConnectionStatus.connecting: - color = Colors.grey; - break; + return _buildConnectingTitleState(context); case ConnectionStatus.disconnected: - color = Colors.grey; - break; + return _buildDisconnectedTitleState(context, _client); + default: + return Offstage(); } - return SvgPicture.asset( - 'svgs/icon_pen_write.svg', - package: 'stream_chat_flutter', - width: 24.0, - height: 24.0, - color: color, - ); }, ), - onPressed: onNewChatButtonTap, ), - ) - ], - title: ValueListenableBuilder( - valueListenable: _client.wsConnectionStatus, - builder: (context, status, child) { - if (titleBuilder != null) { - return titleBuilder(context, status, _client); - } - switch (status) { - case ConnectionStatus.connected: - return _buildConnectedTitleState(context); - case ConnectionStatus.connecting: - return _buildConnectingTitleState(context); - case ConnectionStatus.disconnected: - return _buildDisconnectedTitleState(context, _client); - default: - return Offstage(); - } - }, - ), + ); + }, ); } diff --git a/lib/src/info_tile.dart b/lib/src/info_tile.dart new file mode 100644 index 00000000..c3905b91 --- /dev/null +++ b/lib/src/info_tile.dart @@ -0,0 +1,35 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_portal/flutter_portal.dart'; +import 'package:stream_chat_flutter/src/stream_chat_theme.dart'; + +class InfoTile extends StatelessWidget { + final String message; + final Widget child; + final bool showMessage; + + InfoTile({this.message, this.child, this.showMessage}); + + @override + Widget build(BuildContext context) { + return PortalEntry( + visible: showMessage, + portalAnchor: Alignment.topCenter, + childAnchor: Alignment.bottomCenter, + portal: Container( + height: 25.0, + color: StreamChatTheme.of(context).colorTheme.grey, + child: Center( + child: Text( + message, + style: StreamChatTheme.of(context).textTheme.body.copyWith( + color: Colors.white, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + ), + child: child, + ); + } +} diff --git a/lib/src/stream_chat.dart b/lib/src/stream_chat.dart index 317dfec6..3ebdf6e9 100644 --- a/lib/src/stream_chat.dart +++ b/lib/src/stream_chat.dart @@ -3,6 +3,7 @@ import 'dart:async'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_app_badger/flutter_app_badger.dart'; +import 'package:flutter_portal/flutter_portal.dart'; import 'package:stream_chat/stream_chat.dart'; import 'package:stream_chat_flutter/src/stream_chat_theme.dart'; @@ -67,22 +68,24 @@ class StreamChatState extends State with WidgetsBindingObserver { @override Widget build(BuildContext context) { final theme = _getTheme(context, widget.streamChatThemeData); - return StreamChatTheme( - data: theme, - child: Builder( - builder: (context) { - final materialTheme = Theme.of(context); - final streamTheme = StreamChatTheme.of(context); - return Theme( - data: materialTheme.copyWith( - primaryIconTheme: streamTheme.primaryIconTheme, - accentColor: streamTheme.colorTheme.accentBlue, - scaffoldBackgroundColor: streamTheme.colorTheme.white, - buttonTheme: streamTheme.buttonTheme, - ), - child: widget.child, - ); - }, + return Portal( + child: StreamChatTheme( + data: theme, + child: Builder( + builder: (context) { + final materialTheme = Theme.of(context); + final streamTheme = StreamChatTheme.of(context); + return Theme( + data: materialTheme.copyWith( + primaryIconTheme: streamTheme.primaryIconTheme, + accentColor: streamTheme.colorTheme.accentBlue, + scaffoldBackgroundColor: streamTheme.colorTheme.white, + buttonTheme: streamTheme.buttonTheme, + ), + child: widget.child, + ); + }, + ), ), ); } From 7e8def27072acc5231bb826a5036848b533402a6 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Tue, 12 Jan 2021 18:08:50 +0530 Subject: [PATCH 12/28] [MessageListView] Minor border visibility condition fixes Signed-off-by: Sahil Kumar --- lib/src/message_list_view.dart | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/lib/src/message_list_view.dart b/lib/src/message_list_view.dart index dab8eda1..6242bd8a 100644 --- a/lib/src/message_list_view.dart +++ b/lib/src/message_list_view.dart @@ -732,7 +732,7 @@ class _MessageListViewState extends State { topRight: Radius.circular(16), bottomRight: Radius.circular(16), ), - borderSide: isMyMessage && !isOnlyEmoji ? BorderSide.none : null, + borderSide: isMyMessage || isOnlyEmoji ? BorderSide.none : null, showUserAvatar: isMyMessage ? DisplayWidget.gone : DisplayWidget.show, messageTheme: isMyMessage ? StreamChatTheme.of(context).ownMessageTheme @@ -811,11 +811,13 @@ class _MessageListViewState extends State { final showThreadReplyIndicator = !_isThreadConversation && hasReplies; final isOnlyEmoji = message.text.isOnlyEmoji; + final showMessageBorder = + showThreadReplyIndicator || showInChannelIndicator; final borderSide = isMyMessage - ? !(showThreadReplyIndicator || showInChannelIndicator) + ? !showMessageBorder ? BorderSide.none : null - : isOnlyEmoji + : isOnlyEmoji && !showMessageBorder ? BorderSide.none : null; From 53587a86553342db4d93133ac79d9d71f28f2e07 Mon Sep 17 00:00:00 2001 From: Deven Joshi Date: Tue, 12 Jan 2021 18:14:00 +0530 Subject: [PATCH 13/28] fix: Added alignment customisation --- lib/src/info_tile.dart | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/lib/src/info_tile.dart b/lib/src/info_tile.dart index c3905b91..80595601 100644 --- a/lib/src/info_tile.dart +++ b/lib/src/info_tile.dart @@ -6,15 +6,23 @@ class InfoTile extends StatelessWidget { final String message; final Widget child; final bool showMessage; + final Alignment tileAnchor; + final Alignment childAnchor; - InfoTile({this.message, this.child, this.showMessage}); + InfoTile({ + this.message, + this.child, + this.showMessage, + this.tileAnchor, + this.childAnchor, + }); @override Widget build(BuildContext context) { return PortalEntry( visible: showMessage, - portalAnchor: Alignment.topCenter, - childAnchor: Alignment.bottomCenter, + portalAnchor: tileAnchor ?? Alignment.topCenter, + childAnchor: childAnchor ?? Alignment.bottomCenter, portal: Container( height: 25.0, color: StreamChatTheme.of(context).colorTheme.grey, From 1cf15f1b61484a08af1005b169c270fd54b5469f Mon Sep 17 00:00:00 2001 From: Deven Joshi Date: Tue, 12 Jan 2021 18:17:08 +0530 Subject: [PATCH 14/28] fix: Fixed opacity --- lib/src/info_tile.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/info_tile.dart b/lib/src/info_tile.dart index 80595601..46d111e7 100644 --- a/lib/src/info_tile.dart +++ b/lib/src/info_tile.dart @@ -25,7 +25,7 @@ class InfoTile extends StatelessWidget { childAnchor: childAnchor ?? Alignment.bottomCenter, portal: Container( height: 25.0, - color: StreamChatTheme.of(context).colorTheme.grey, + color: StreamChatTheme.of(context).colorTheme.grey.withOpacity(0.9), child: Center( child: Text( message, From 0bb76bab39484dc7ef87b63600fa811e0fd7b36a Mon Sep 17 00:00:00 2001 From: Deven Joshi Date: Tue, 12 Jan 2021 18:18:33 +0530 Subject: [PATCH 15/28] fix: Added better customisation --- lib/src/info_tile.dart | 27 ++++++++++++++++----------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/lib/src/info_tile.dart b/lib/src/info_tile.dart index 46d111e7..2e12d6b3 100644 --- a/lib/src/info_tile.dart +++ b/lib/src/info_tile.dart @@ -8,14 +8,17 @@ class InfoTile extends StatelessWidget { final bool showMessage; final Alignment tileAnchor; final Alignment childAnchor; + final TextStyle textStyle; + final Color backgroundColor; - InfoTile({ - this.message, - this.child, - this.showMessage, - this.tileAnchor, - this.childAnchor, - }); + InfoTile( + {this.message, + this.child, + this.showMessage, + this.tileAnchor, + this.childAnchor, + this.textStyle, + this.backgroundColor}); @override Widget build(BuildContext context) { @@ -25,13 +28,15 @@ class InfoTile extends StatelessWidget { childAnchor: childAnchor ?? Alignment.bottomCenter, portal: Container( height: 25.0, - color: StreamChatTheme.of(context).colorTheme.grey.withOpacity(0.9), + color: backgroundColor ?? + StreamChatTheme.of(context).colorTheme.grey.withOpacity(0.9), child: Center( child: Text( message, - style: StreamChatTheme.of(context).textTheme.body.copyWith( - color: Colors.white, - ), + style: textStyle ?? + StreamChatTheme.of(context).textTheme.body.copyWith( + color: Colors.white, + ), maxLines: 1, overflow: TextOverflow.ellipsis, ), From 40910f93f1e82b49291e44e0868d1a62b8fbccca Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Tue, 12 Jan 2021 13:49:44 +0100 Subject: [PATCH 16/28] fix reaction picker spacing and size --- example/pubspec.yaml | 2 +- lib/src/reaction_picker.dart | 123 ++++++++++++++++++++--------------- 2 files changed, 71 insertions(+), 54 deletions(-) diff --git a/example/pubspec.yaml b/example/pubspec.yaml index 46dc8820..6c4ae4dc 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -1,6 +1,6 @@ name: example description: A new Flutter project. -version: 1.1.0+2 +version: 1.1.0+3 environment: sdk: ">=2.2.2 <3.0.0" diff --git a/lib/src/reaction_picker.dart b/lib/src/reaction_picker.dart index bca74a99..804b1559 100644 --- a/lib/src/reaction_picker.dart +++ b/lib/src/reaction_picker.dart @@ -5,6 +5,7 @@ import 'package:flutter/material.dart'; import 'package:stream_chat_flutter/src/stream_svg_icon.dart'; import '../stream_chat_flutter.dart'; +import 'extension.dart'; /// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/screenshots/reaction_picker.png) /// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/screenshots/reaction_picker_paint.png) @@ -57,67 +58,83 @@ class _ReactionPickerState extends State return Transform.scale( scale: val, child: Material( + borderRadius: BorderRadius.circular(24), color: StreamChatTheme.of(context).colorTheme.white, clipBehavior: Clip.hardEdge, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(24), - ), child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 8.0), + padding: const EdgeInsets.symmetric( + horizontal: 16.0, + vertical: 8.0, + ), child: Row( crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.end, mainAxisSize: MainAxisSize.min, - children: reactionIcons.map((reactionIcon) { - final ownReactionIndex = widget.message.ownReactions - ?.indexWhere((reaction) => - reaction.type == reactionIcon.type) ?? - -1; - var index = reactionIcons.indexOf(reactionIcon); + children: reactionIcons + .map((reactionIcon) { + final ownReactionIndex = widget.message.ownReactions + ?.indexWhere((reaction) => + reaction.type == reactionIcon.type) ?? + -1; + var index = reactionIcons.indexOf(reactionIcon); - return IconButton( - iconSize: 24, - icon: AnimatedBuilder( - animation: animations[index], - builder: (context, val) { - return Transform.scale( - scale: animations[index].value, - child: StreamSvgIcon( - assetName: reactionIcon.assetName, - height: max( - 0, - animations[index].value * 24.0, - ), - width: max( - 0, - animations[index].value * 24.0, - ), - color: ownReactionIndex != -1 - ? StreamChatTheme.of(context) - .colorTheme - .accentBlue - : Theme.of(context) - .iconTheme - .color - .withOpacity(.5), - ), - ); - }), - onPressed: () { - if (ownReactionIndex != -1) { - removeReaction( - context, - widget.message.ownReactions[ownReactionIndex], - ); - } else { - sendReaction( - context, - reactionIcon.type, - ); - } - }, - ); - }).toList(), + return RawMaterialButton( + elevation: 0, + padding: const EdgeInsets.all(0), + clipBehavior: Clip.none, + shape: ContinuousRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), + constraints: BoxConstraints.tightFor( + height: 24, + width: 24, + ), + child: AnimatedBuilder( + animation: animations[index], + builder: (context, val) { + return Transform.scale( + alignment: Alignment.center, + scale: animations[index].value, + child: StreamSvgIcon( + assetName: reactionIcon.assetName, + height: max( + 0, + animations[index].value * 24.0, + ), + width: max( + 0, + animations[index].value * 24.0, + ), + color: ownReactionIndex != -1 + ? StreamChatTheme.of(context) + .colorTheme + .accentBlue + : Theme.of(context) + .iconTheme + .color + .withOpacity(.5), + ), + ); + }), + onPressed: () { + if (ownReactionIndex != -1) { + removeReaction( + context, + widget.message.ownReactions[ownReactionIndex], + ); + } else { + sendReaction( + context, + reactionIcon.type, + ); + } + }, + ); + }) + .insertBetween(SizedBox( + width: 16, + )) + .toList(), ), ), ), From 294c9edafb455813ceda4cb74cb9ed13091c199e Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Tue, 12 Jan 2021 14:35:29 +0100 Subject: [PATCH 17/28] fix reaction picker spacing and size --- lib/src/reaction_picker.dart | 96 +++++++++++++++++++----------------- 1 file changed, 51 insertions(+), 45 deletions(-) diff --git a/lib/src/reaction_picker.dart b/lib/src/reaction_picker.dart index 804b1559..b0aa51ce 100644 --- a/lib/src/reaction_picker.dart +++ b/lib/src/reaction_picker.dart @@ -78,57 +78,63 @@ class _ReactionPickerState extends State -1; var index = reactionIcons.indexOf(reactionIcon); - return RawMaterialButton( - elevation: 0, - padding: const EdgeInsets.all(0), - clipBehavior: Clip.none, - shape: ContinuousRectangleBorder( - borderRadius: BorderRadius.circular(16), - ), + return ConstrainedBox( constraints: BoxConstraints.tightFor( height: 24, width: 24, ), - child: AnimatedBuilder( - animation: animations[index], - builder: (context, val) { - return Transform.scale( - alignment: Alignment.center, - scale: animations[index].value, - child: StreamSvgIcon( - assetName: reactionIcon.assetName, - height: max( - 0, - animations[index].value * 24.0, + child: RawMaterialButton( + elevation: 0, + padding: const EdgeInsets.all(0), + clipBehavior: Clip.none, + shape: ContinuousRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), + constraints: BoxConstraints.tightFor( + height: 24, + width: 24, + ), + child: AnimatedBuilder( + animation: animations[index], + builder: (context, val) { + return Transform.scale( + alignment: Alignment.center, + scale: animations[index].value, + child: StreamSvgIcon( + assetName: reactionIcon.assetName, + height: max( + 0, + animations[index].value * 24.0, + ), + width: max( + 0, + animations[index].value * 24.0, + ), + color: ownReactionIndex != -1 + ? StreamChatTheme.of(context) + .colorTheme + .accentBlue + : Theme.of(context) + .iconTheme + .color + .withOpacity(.5), ), - width: max( - 0, - animations[index].value * 24.0, - ), - color: ownReactionIndex != -1 - ? StreamChatTheme.of(context) - .colorTheme - .accentBlue - : Theme.of(context) - .iconTheme - .color - .withOpacity(.5), - ), + ); + }), + onPressed: () { + if (ownReactionIndex != -1) { + removeReaction( + context, + widget.message.ownReactions[ownReactionIndex], ); - }), - onPressed: () { - if (ownReactionIndex != -1) { - removeReaction( - context, - widget.message.ownReactions[ownReactionIndex], - ); - } else { - sendReaction( - context, - reactionIcon.type, - ); - } - }, + } else { + sendReaction( + context, + reactionIcon.type, + ); + } + }, + ), ); }) .insertBetween(SizedBox( From 04902f6a8124c40e68d325c9987c6e28e79ee400 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Tue, 12 Jan 2021 14:50:47 +0100 Subject: [PATCH 18/28] fix indicator --- lib/src/message_widget.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/message_widget.dart b/lib/src/message_widget.dart index 8118af01..ccf7dc83 100644 --- a/lib/src/message_widget.dart +++ b/lib/src/message_widget.dart @@ -428,7 +428,7 @@ class _MessageWidgetState extends State { if (widget.showReactionPickerIndicator) Positioned( right: 0, - top: -6, + top: -8, child: Transform( transform: Matrix4.rotationY( widget.reverse ? pi : 0), From 16bedf87287d845a2a765f114b016c62419ef9d6 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Tue, 12 Jan 2021 19:36:12 +0530 Subject: [PATCH 19/28] [QuotedMessageWidget, MessageWidget] Fix quoted message padding in case of attachments Signed-off-by: Sahil Kumar --- lib/src/message_widget.dart | 33 +++++++++++++++++++++-------- lib/src/quoted_message_widget.dart | 34 ++++++++++++------------------ 2 files changed, 38 insertions(+), 29 deletions(-) diff --git a/lib/src/message_widget.dart b/lib/src/message_widget.dart index c4b03ce2..8f4b0158 100644 --- a/lib/src/message_widget.dart +++ b/lib/src/message_widget.dart @@ -272,6 +272,12 @@ class _MessageWidgetState extends State { widget.message.attachments?.any((element) => element.type == 'giphy') == true; + bool get hasNonUrlAttachments => + widget.message.attachments + ?.where((it) => it.ogScrapeUrl == null) + ?.isNotEmpty == + true; + bool get showBottomRow => showThreadReplyIndicator || showUsername || @@ -409,8 +415,9 @@ class _MessageWidgetState extends State { children: [ if (hasQuotedMessage) _buildQuotedMessage(), - ..._parseAttachments( - context), + if (hasNonUrlAttachments) + ..._parseAttachments( + context), if (widget.message.text .trim() .isNotEmpty && @@ -487,13 +494,21 @@ class _MessageWidgetState extends State { widget.onQuotedMessageTap != null ? () => widget.onQuotedMessageTap(widget.message.quotedMessageId) : null; - return QuotedMessageWidget( - onTap: onTap, - message: widget.message.quotedMessage, - messageTheme: isMyMessage - ? StreamChatTheme.of(context).otherMessageTheme - : StreamChatTheme.of(context).ownMessageTheme, - reverse: widget.reverse, + return Padding( + 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, + ), ); } diff --git a/lib/src/quoted_message_widget.dart b/lib/src/quoted_message_widget.dart index c718cdb5..309df703 100644 --- a/lib/src/quoted_message_widget.dart +++ b/lib/src/quoted_message_widget.dart @@ -109,17 +109,14 @@ class QuotedMessageWidget extends StatelessWidget { Widget build(BuildContext context) { return InkWell( onTap: onTap, - child: Padding( - padding: const EdgeInsets.only(top: 8, right: 4, left: 8), - child: Row( - crossAxisAlignment: CrossAxisAlignment.end, - mainAxisSize: MainAxisSize.min, - children: [ - Flexible(child: _buildMessage(context)), - SizedBox(width: 4), - _buildUserAvatar(), - ], - ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.end, + mainAxisSize: MainAxisSize.min, + children: [ + Flexible(child: _buildMessage(context)), + SizedBox(width: 8), + _buildUserAvatar(), + ], ), ); } @@ -247,16 +244,13 @@ class QuotedMessageWidget extends StatelessWidget { return Transform( transform: Matrix4.rotationY(reverse ? pi : 0), alignment: Alignment.center, - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 4.0), - child: UserAvatar( - user: message.user, - constraints: BoxConstraints.tightFor( - height: 24, - width: 24, - ), - showOnlineStatus: false, + child: UserAvatar( + user: message.user, + constraints: BoxConstraints.tightFor( + height: 24, + width: 24, ), + showOnlineStatus: false, ), ); } From 80d8c48826a7f1da7b9ab8f6bd6813bdc28e0ea2 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Tue, 12 Jan 2021 19:46:19 +0530 Subject: [PATCH 20/28] Minor ui fixes Signed-off-by: Sahil Kumar --- lib/src/message_actions_modal.dart | 6 ++---- lib/src/message_input.dart | 13 ++++++++----- lib/src/utils.dart | 22 ++++++++-------------- 3 files changed, 18 insertions(+), 23 deletions(-) diff --git a/lib/src/message_actions_modal.dart b/lib/src/message_actions_modal.dart index 7f9c5e85..2d125bab 100644 --- a/lib/src/message_actions_modal.dart +++ b/lib/src/message_actions_modal.dart @@ -164,7 +164,7 @@ class _MessageActionsModalState extends State { ), Padding( padding: EdgeInsets.only( - right: widget.reverse ? 16 : 0, + right: widget.reverse ? 8 : 0, left: widget.reverse ? 0 : 48, ), child: SizedBox( @@ -448,9 +448,7 @@ class _MessageActionsModalState extends State { ), ], ), - onTap: () { - _showFlagDialog(); - }, + onTap: () => _showFlagDialog(), ); } diff --git a/lib/src/message_input.dart b/lib/src/message_input.dart index 8bb20302..4561372e 100644 --- a/lib/src/message_input.dart +++ b/lib/src/message_input.dart @@ -1525,11 +1525,14 @@ class MessageInputState extends State { return Transform( transform: Matrix4.rotationY(pi), alignment: Alignment.center, - child: QuotedMessageWidget( - reverse: true, - showBorder: !containsUrl, - message: widget.quotedMessage, - messageTheme: StreamChatTheme.of(context).otherMessageTheme, + child: Padding( + padding: const EdgeInsets.fromLTRB(8, 8, 8, 0), + child: QuotedMessageWidget( + reverse: true, + showBorder: !containsUrl, + message: widget.quotedMessage, + messageTheme: StreamChatTheme.of(context).otherMessageTheme, + ), ), ); } diff --git a/lib/src/utils.dart b/lib/src/utils.dart index 6548b987..d1dec9c8 100644 --- a/lib/src/utils.dart +++ b/lib/src/utils.dart @@ -37,27 +37,21 @@ Future showConfirmationDialog( return Column( mainAxisSize: MainAxisSize.min, children: [ - SizedBox( - height: 26.0, - ), + SizedBox(height: 26.0), if (icon != null) icon, - SizedBox( - height: 26.0, - ), + SizedBox(height: 26.0), Text( title, style: StreamChatTheme.of(context).textTheme.headlineBold, ), - SizedBox( - height: 7.0, - ), - Text(question), - SizedBox( - height: 36.0, + SizedBox(height: 7.0), + Text( + question, + textAlign: TextAlign.center, ), + SizedBox(height: 36.0), Container( - color: - StreamChatTheme.of(context).colorTheme.black.withOpacity(.08), + color: StreamChatTheme.of(context).colorTheme.greyGainsboro, height: 1.0, ), Row( From 74e9ec648c0ea56b398cc459e21a33342d025d45 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Tue, 12 Jan 2021 15:24:53 +0100 Subject: [PATCH 21/28] remove add member for distinct channels --- example/lib/group_info_screen.dart | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/example/lib/group_info_screen.dart b/example/lib/group_info_screen.dart index ed5f2c61..bde4c0df 100644 --- a/example/lib/group_info_screen.dart +++ b/example/lib/group_info_screen.dart @@ -137,20 +137,21 @@ class _GroupInfoScreenState extends State { ), centerTitle: true, actions: [ - StreamNeumorphicButton( - child: InkWell( - onTap: () { - _buildAddUserModal(context); - }, - child: Padding( - padding: const EdgeInsets.all(8.0), - child: StreamSvgIcon.userAdd( - color: StreamChatTheme.of(context) - .colorTheme - .accentBlue), + if (!channel.channel.isDistinct) + StreamNeumorphicButton( + child: InkWell( + onTap: () { + _buildAddUserModal(context); + }, + child: Padding( + padding: const EdgeInsets.all(8.0), + child: StreamSvgIcon.userAdd( + color: StreamChatTheme.of(context) + .colorTheme + .accentBlue), + ), ), ), - ), ], ), body: ListView( From 18d3cbcc86cad354e5fe1785a0ecc7249593dbab Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Tue, 12 Jan 2021 15:25:25 +0100 Subject: [PATCH 22/28] version bump --- example/pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/example/pubspec.yaml b/example/pubspec.yaml index 6c4ae4dc..1605cdd5 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -1,6 +1,6 @@ name: example description: A new Flutter project. -version: 1.1.0+3 +version: 1.1.1 environment: sdk: ">=2.2.2 <3.0.0" From fad38a513ac0a50ca521363af74132063a618044 Mon Sep 17 00:00:00 2001 From: Deven Joshi Date: Tue, 12 Jan 2021 21:13:38 +0530 Subject: [PATCH 23/28] feat: Added a way to disable info tiles and added info tile to message list view --- lib/src/channel_header.dart | 7 +- lib/src/channel_list_header.dart | 7 +- lib/src/message_list_view.dart | 345 +++++++++++++++++-------------- 3 files changed, 201 insertions(+), 158 deletions(-) diff --git a/lib/src/channel_header.dart b/lib/src/channel_header.dart index fe624473..8b920f23 100644 --- a/lib/src/channel_header.dart +++ b/lib/src/channel_header.dart @@ -69,6 +69,8 @@ class ChannelHeader extends StatelessWidget implements PreferredSizeWidget { /// If true the typing indicator will be rendered if a user is typing final bool showTypingIndicator; + final bool showConnectionStateTile; + /// Creates a channel header ChannelHeader({ Key key, @@ -77,6 +79,7 @@ class ChannelHeader extends StatelessWidget implements PreferredSizeWidget { this.onTitleTap, this.showTypingIndicator = true, this.onImageTap, + this.showConnectionStateTile = false, }) : preferredSize = Size.fromHeight(kToolbarHeight), super(key: key); @@ -97,7 +100,7 @@ class ChannelHeader extends StatelessWidget implements PreferredSizeWidget { showStatus = false; break; case ConnectionStatus.connecting: - statusString = 'Connecting'; + statusString = 'Reconnecting...'; break; case ConnectionStatus.disconnected: statusString = 'Disconnected'; @@ -105,7 +108,7 @@ class ChannelHeader extends StatelessWidget implements PreferredSizeWidget { } return InfoTile( - showMessage: showStatus, + showMessage: showConnectionStateTile ? showStatus : false, message: statusString, child: AppBar( brightness: Theme.of(context).brightness, diff --git a/lib/src/channel_list_header.dart b/lib/src/channel_list_header.dart index e7fc9f9a..b6bf3ca7 100644 --- a/lib/src/channel_list_header.dart +++ b/lib/src/channel_list_header.dart @@ -54,6 +54,7 @@ class ChannelListHeader extends StatelessWidget implements PreferredSizeWidget { this.titleBuilder, this.onUserAvatarTap, this.onNewChatButtonTap, + this.showConnectionStateTile = false, }) : super(key: key); /// Pass this if you don't have a [Client] in your widget tree. @@ -69,6 +70,8 @@ class ChannelListHeader extends StatelessWidget implements PreferredSizeWidget { /// Callback to call when pressing the new chat button. final VoidCallback onNewChatButtonTap; + final bool showConnectionStateTile; + @override Widget build(BuildContext context) { final _client = client ?? StreamChat.of(context).client; @@ -85,7 +88,7 @@ class ChannelListHeader extends StatelessWidget implements PreferredSizeWidget { showStatus = false; break; case ConnectionStatus.connecting: - statusString = 'Connecting'; + statusString = 'Reconnecting...'; break; case ConnectionStatus.disconnected: statusString = 'Disconnected'; @@ -93,7 +96,7 @@ class ChannelListHeader extends StatelessWidget implements PreferredSizeWidget { } return InfoTile( - showMessage: showStatus, + showMessage: showConnectionStateTile ? showStatus : false, message: statusString, child: AppBar( brightness: Theme.of(context).brightness, diff --git a/lib/src/message_list_view.dart b/lib/src/message_list_view.dart index cda0d64c..f1d2ca12 100644 --- a/lib/src/message_list_view.dart +++ b/lib/src/message_list_view.dart @@ -7,6 +7,7 @@ import 'package:jiffy/jiffy.dart'; import 'package:rxdart/rxdart.dart'; import 'package:scrollable_positioned_list/scrollable_positioned_list.dart'; import 'package:stream_chat/stream_chat.dart'; +import 'package:stream_chat_flutter/src/info_tile.dart'; import 'package:stream_chat_flutter/src/lazy_load_scroll_view.dart'; import 'package:stream_chat_flutter/src/message_widget.dart'; import 'package:stream_chat_flutter/src/stream_svg_icon.dart'; @@ -124,6 +125,7 @@ class MessageListView extends StatefulWidget { this.highlightInitialMessage = false, this.messageHighlightColor, this.onShowMessage, + this.showConnectionStateTile = false, }) : super(key: key); /// Function used to build a custom message widget @@ -181,6 +183,8 @@ class MessageListView extends StatefulWidget { final ShowMessageCallback onShowMessage; + final bool showConnectionStateTile; + @override _MessageListViewState createState() => _MessageListViewState(); } @@ -297,172 +301,205 @@ class _MessageListViewState extends State { } _messageListLength = newMessagesListLength; + final _client = StreamChat.of(context).client; return Stack( alignment: Alignment.center, children: [ - LazyLoadScrollView( - 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 == messages.length + 2) return Offstage(); - if (i == messages.length + 1) return Offstage(); - if (i == 0) return SizedBox(height: 30); - 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( + ValueListenableBuilder( + valueListenable: _client.wsConnectionStatus, + builder: (context, status, _) { + String statusString = ''; + bool showStatus = true; + + switch (status) { + case ConnectionStatus.connected: + statusString = 'Connected'; + showStatus = false; + break; + case ConnectionStatus.connecting: + statusString = 'Reconnecting...'; + break; + case ConnectionStatus.disconnected: + statusString = 'Disconnected'; + break; + } + + return InfoTile( + showMessage: + widget.showConnectionStateTile ? showStatus : false, + tileAnchor: Alignment.topCenter, + childAnchor: Alignment.topCenter, + message: statusString, + child: LazyLoadScrollView( + 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 == messages.length + 2) return Offstage(); + if (i == messages.length + 1) return Offstage(); + if (i == 0) return SizedBox(height: 30); + final message = messages[i]; + final nextMessage = messages[i - 1]; + if (!Jiffy(message.createdAt.toLocal()).isSame( nextMessage.createdAt.toLocal(), - ) - : DateDivider( - dateTime: 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, ); - 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 Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - buildParentMessage(widget.parentMessage), - Container( - decoration: BoxDecoration( - gradient: StreamChatTheme.of(context) - .colorTheme - .bgGradient, - ), - child: Padding( - padding: const EdgeInsets.all(8.0), - child: Text( - '${widget.parentMessage.replyCount} ${widget.parentMessage.replyCount == 1 ? 'Reply' : 'Replies'}', - textAlign: TextAlign.center, - style: StreamChatTheme.of(context) - .channelTheme - .channelHeaderTheme - .lastMessageAt, - ), - ), - ), - ], - ); - } - } - if (i == messages.length + 1) { - return _buildLoadingIndicator( - streamChannel, - QueryDirection.top, - ); - } - if (i == 0) { - return _buildLoadingIndicator( - streamChannel, - QueryDirection.bottom, - ); - } - final message = messages[i - 1]; + 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 Column( + crossAxisAlignment: + CrossAxisAlignment.stretch, + children: [ + buildParentMessage(widget.parentMessage), + Container( + decoration: BoxDecoration( + gradient: StreamChatTheme.of(context) + .colorTheme + .bgGradient, + ), + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Text( + '${widget.parentMessage.replyCount} ${widget.parentMessage.replyCount == 1 ? 'Reply' : 'Replies'}', + textAlign: TextAlign.center, + style: StreamChatTheme.of(context) + .channelTheme + .channelHeaderTheme + .lastMessageAt, + ), + ), + ), + ], + ); + } + } + 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; + 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 (widget.messageBuilder != null) { - messageWidget = Builder( - key: ValueKey('MESSAGE-${message.id}'), - builder: (context) => widget.messageBuilder( - context, - MessageDetails( + if (i == 1) { + messageWidget = _buildBottomMessage( context, message, messages, - i, - ), - messages), - ); - } else { - messageWidget = buildMessage(message, messages, i); - } - } - return messageWidget; - }, - ), - ), + 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 { + messageWidget = + buildMessage(message, messages, i); + } + } + return messageWidget; + }, + ), + ), + ); + }), if (widget.showScrollToBottom) _buildScrollToBottom(), Positioned( top: 20.0, From 3e1766940acf177489f6b6031807252102ce2df6 Mon Sep 17 00:00:00 2001 From: Deven Joshi Date: Tue, 12 Jan 2021 21:24:46 +0530 Subject: [PATCH 24/28] feat: Added error tile for message search list view --- lib/src/message_search_list_view.dart | 76 +++++++++++++++------------ lib/stream_chat_flutter.dart | 1 + 2 files changed, 44 insertions(+), 33 deletions(-) diff --git a/lib/src/message_search_list_view.dart b/lib/src/message_search_list_view.dart index 73a98a2d..b176e148 100644 --- a/lib/src/message_search_list_view.dart +++ b/lib/src/message_search_list_view.dart @@ -2,6 +2,7 @@ import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:stream_chat/stream_chat.dart'; +import 'package:stream_chat_flutter/src/info_tile.dart'; import 'package:stream_chat_flutter/src/message_search_item.dart'; import '../stream_chat_flutter.dart'; @@ -63,6 +64,7 @@ class MessageSearchListView extends StatefulWidget { this.onItemTap, this.showResultCount = true, this.pullToRefresh = true, + this.showErrorTile = false, }) : super(key: key); /// Message String to search on @@ -111,6 +113,8 @@ class MessageSearchListView extends StatefulWidget { /// Set it to false to disable the pull-to-refresh widget final bool pullToRefresh; + final bool showErrorTile; + @override _MessageSearchListViewState createState() => _MessageSearchListViewState(); } @@ -205,41 +209,47 @@ class _MessageSearchListViewState extends State { message = 'Check your connection and retry'; } } - return Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text.rich( - TextSpan( - children: [ - WidgetSpan( - child: Padding( - padding: const EdgeInsets.only(right: 2.0), - child: Icon(Icons.error_outline), + return InfoTile( + showMessage: widget.showErrorTile, + tileAnchor: Alignment.topCenter, + childAnchor: Alignment.topCenter, + message: 'An error occurred.', + child: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text.rich( + TextSpan( + children: [ + WidgetSpan( + child: Padding( + padding: const EdgeInsets.only(right: 2.0), + child: Icon(Icons.error_outline), + ), ), - ), - TextSpan(text: 'Error loading messages'), - ], + TextSpan(text: 'Error loading messages'), + ], + ), + style: Theme.of(context).textTheme.headline6, ), - style: Theme.of(context).textTheme.headline6, - ), - Padding( - padding: const EdgeInsets.only(top: 16.0), - child: Text(message), - ), - RaisedButton( - onPressed: () { - messageSearchBloc.search( - filter: widget.filters, - sort: widget.sortOptions, - query: widget.messageQuery, - pagination: widget.paginationParams, - messageFilter: widget.messageFilters, - ); - }, - child: Text('Retry'), - ), - ], + Padding( + padding: const EdgeInsets.only(top: 16.0), + child: Text(message), + ), + RaisedButton( + onPressed: () { + messageSearchBloc.search( + filter: widget.filters, + sort: widget.sortOptions, + query: widget.messageQuery, + pagination: widget.paginationParams, + messageFilter: widget.messageFilters, + ); + }, + child: Text('Retry'), + ), + ], + ), ), ); } diff --git a/lib/stream_chat_flutter.dart b/lib/stream_chat_flutter.dart index 09eab2a4..df113234 100644 --- a/lib/stream_chat_flutter.dart +++ b/lib/stream_chat_flutter.dart @@ -46,3 +46,4 @@ export 'src/unread_indicator.dart'; export 'src/option_list_tile.dart'; export 'src/channel_file_display_screen.dart'; export 'src/channel_media_display_screen.dart'; +export 'src/info_tile.dart'; From 4aa224a5e6dba291da80dac2c0c6a9a286abe62f Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Tue, 12 Jan 2021 16:58:52 +0100 Subject: [PATCH 25/28] fix messageinput focus --- example/lib/main.dart | 7 +++++-- example/pubspec.yaml | 2 +- lib/src/message_input.dart | 5 ++++- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/example/lib/main.dart b/example/lib/main.dart index 98fa71a2..49ee3fdc 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -623,8 +623,8 @@ class _ChannelPageState extends State { @override void initState() { - super.initState(); _focusNode = FocusNode(); + super.initState(); } @override @@ -635,7 +635,9 @@ class _ChannelPageState extends State { void _reply(Message message) { setState(() => _quotedMessage = message); - _focusNode.requestFocus(); + WidgetsBinding.instance.addPostFrameCallback((timeStamp) { + _focusNode.requestFocus(); + }); } @override @@ -752,6 +754,7 @@ class _ChannelPageState extends State { quotedMessage: _quotedMessage, onQuotedMessageCleared: () { setState(() => _quotedMessage = null); + _focusNode.unfocus(); }, ), ], diff --git a/example/pubspec.yaml b/example/pubspec.yaml index 1605cdd5..2b8859ea 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -1,6 +1,6 @@ name: example description: A new Flutter project. -version: 1.1.1 +version: 1.1.1+1 environment: sdk: ">=2.2.2 <3.0.0" diff --git a/lib/src/message_input.dart b/lib/src/message_input.dart index 4561372e..cc12fbd9 100644 --- a/lib/src/message_input.dart +++ b/lib/src/message_input.dart @@ -203,6 +203,8 @@ class MessageInputState extends State { bool _openFilePickerSection = false; int _filePickerIndex = 0; double _filePickerSize = _kMinMediaPickerSize; + KeyboardVisibilityController _keyboardVisibilityController = + KeyboardVisibilityController(); /// The editing controller passed to the input TextField TextEditingController textEditingController; @@ -2249,7 +2251,8 @@ class MessageInputState extends State { _emojiNames = Emoji.all().map((e) => e.name); if (!kIsWeb) { - _keyboardListener = KeyboardVisibility.onChange.listen((visible) { + _keyboardListener = + _keyboardVisibilityController.onChange.listen((visible) { if (_focusNode.hasFocus) { _onChanged(context, textEditingController.text); } From f3ff7588946e8eae6c2a0defa789b4171ef949d4 Mon Sep 17 00:00:00 2001 From: Deven Joshi Date: Wed, 13 Jan 2021 13:14:36 +0530 Subject: [PATCH 26/28] feat: Added error message to new group and info tiles to new chat and merged --- example/lib/group_chat_details_screen.dart | 104 ++++- example/lib/new_chat_screen.dart | 500 +++++++++++---------- lib/src/message_list_view.dart | 334 +++++++------- 3 files changed, 541 insertions(+), 397 deletions(-) diff --git a/example/lib/group_chat_details_screen.dart b/example/lib/group_chat_details_screen.dart index 31ccc703..f59b5c4f 100644 --- a/example/lib/group_chat_details_screen.dart +++ b/example/lib/group_chat_details_screen.dart @@ -123,23 +123,28 @@ class _GroupChatDetailsScreenState extends State { onPressed: _isGroupNameEmpty ? null : () async { - final groupName = _groupNameController.text; - final client = StreamChat.of(context).client; - final channel = client - .channel('messaging', id: Uuid().v4(), extraData: { - 'members': [ - client.state.user.id, - ..._selectedUsers.map((e) => e.id), - ], - 'name': groupName, - }); - await channel.watch(); - Navigator.pushNamedAndRemoveUntil( - context, - Routes.CHANNEL_PAGE, - ModalRoute.withName(Routes.HOME), - arguments: ChannelPageArgs(channel: channel), - ); + try { + final groupName = _groupNameController.text; + final client = StreamChat.of(context).client; + final channel = client.channel('messaging', + id: Uuid().v4(), + extraData: { + 'members': [ + client.state.user.id, + ..._selectedUsers.map((e) => e.id), + ], + 'name': groupName, + }); + await channel.watch(); + Navigator.pushNamedAndRemoveUntil( + context, + Routes.CHANNEL_PAGE, + ModalRoute.withName(Routes.HOME), + arguments: ChannelPageArgs(channel: channel), + ); + } catch (err) { + _showErrorAlert(); + } }, ), ), @@ -227,4 +232,69 @@ class _GroupChatDetailsScreenState extends State { ), ); } + + void _showErrorAlert() { + showModalBottomSheet( + backgroundColor: StreamChatTheme.of(context).colorTheme.white, + context: context, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.only( + topLeft: Radius.circular(16.0), + topRight: Radius.circular(16.0), + )), + builder: (context) { + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + SizedBox( + height: 26.0, + ), + StreamSvgIcon.error( + color: StreamChatTheme.of(context).colorTheme.accentRed, + size: 24.0, + ), + SizedBox( + height: 26.0, + ), + Text( + 'Something went wrong', + style: StreamChatTheme.of(context).textTheme.headlineBold, + ), + SizedBox( + height: 7.0, + ), + Text('The operation couldn\'t be completed.'), + SizedBox( + height: 36.0, + ), + Container( + color: + StreamChatTheme.of(context).colorTheme.black.withOpacity(.08), + height: 1.0, + ), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + FlatButton( + child: Text( + 'OK', + style: StreamChatTheme.of(context) + .textTheme + .bodyBold + .copyWith( + color: StreamChatTheme.of(context) + .colorTheme + .accentBlue), + ), + onPressed: () { + Navigator.of(context).pop(); + }, + ), + ], + ), + ], + ); + }, + ); + } } diff --git a/example/lib/new_chat_screen.dart b/example/lib/new_chat_screen.dart index 1de36b46..5772f5e2 100644 --- a/example/lib/new_chat_screen.dart +++ b/example/lib/new_chat_screen.dart @@ -134,248 +134,288 @@ class _NewChatScreenState extends State { ), centerTitle: true, ), - body: StreamChannel( - showLoading: false, - channel: channel, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - ChipsInputTextField( - key: _chipInputTextFieldStateKey, - controller: _controller, - focusNode: _searchFocusNode, - chipBuilder: (context, user) { - return GestureDetector( - onTap: () { - _chipInputTextFieldState.removeItem(user); - _searchFocusNode.requestFocus(); - }, - child: Stack( - alignment: AlignmentDirectional.centerStart, - children: [ - Container( - decoration: BoxDecoration( - color: StreamChatTheme.of(context) - .colorTheme - .greyGainsboro, - borderRadius: BorderRadius.circular(12), - ), - padding: const EdgeInsets.only(left: 24), - child: Padding( - padding: const EdgeInsets.fromLTRB(8, 4, 12, 4), - child: Text( - user.name, - maxLines: 1, - style: TextStyle( - color: - StreamChatTheme.of(context).colorTheme.black, - ), - ), - ), - ), - Container( - foregroundDecoration: BoxDecoration( - color: StreamChatTheme.of(context).colorTheme.overlay, - shape: BoxShape.circle, - ), - child: UserAvatar( - showOnlineStatus: false, - user: user, - constraints: BoxConstraints.tightFor( - height: 24, - width: 24, - ), - ), - ), - StreamSvgIcon.close(), - ], - ), - ); - }, - onChipAdded: (user) { - setState(() => _selectedUsers.add(user)); - }, - onChipRemoved: (user) { - setState(() => _selectedUsers.remove(user)); - }, - ), - if (!_isSearchActive && !_selectedUsers.isNotEmpty) - Container( - child: InkWell( - onTap: () { - Navigator.pushNamed( - context, - Routes.NEW_GROUP_CHAT, - ); - }, - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 8), - child: Row( - children: [ - StreamNeumorphicButton( - child: Center( - child: StreamSvgIcon.contacts( - color: StreamChatTheme.of(context) - .colorTheme - .accentBlue, - size: 24, - ), - ), - ), - SizedBox(width: 8), - Text( - 'Create a Group', - style: StreamChatTheme.of(context).textTheme.bodyBold, - ), - ], - ), - ), - ), - ), - if (_showUserList) - Container( - width: double.maxFinite, - decoration: BoxDecoration( - gradient: StreamChatTheme.of(context).colorTheme.bgGradient, - ), - child: Padding( - padding: const EdgeInsets.symmetric( - vertical: 8, - horizontal: 8, - ), - child: Text( - _isSearchActive - ? "Matches for \"$_userNameQuery\"" - : 'On the platform', - style: StreamChatTheme.of(context) - .textTheme - .footnote - .copyWith( - color: StreamChatTheme.of(context) - .colorTheme - .black - .withOpacity(.5))), - ), - ), - Expanded( - child: _showUserList - ? GestureDetector( - behavior: HitTestBehavior.opaque, - onPanDown: (_) => FocusScope.of(context).unfocus(), - child: UsersBloc( - child: UserListView( - selectedUsers: _selectedUsers, - groupAlphabetically: _isSearchActive ? false : true, - onUserTap: (user, _) { - _controller.clear(); - if (!_selectedUsers.contains(user)) { - _chipInputTextFieldState - ..addItem(user) - ..pauseItemAddition(); - } else { - _chipInputTextFieldState.removeItem(user); - } + body: ValueListenableBuilder( + valueListenable: StreamChat.of(context).client.wsConnectionStatus, + builder: (context, status, _) { + String statusString = ''; + bool showStatus = true; + + switch (status) { + case ConnectionStatus.connected: + statusString = 'Connected'; + showStatus = false; + break; + case ConnectionStatus.connecting: + statusString = 'Reconnecting...'; + break; + case ConnectionStatus.disconnected: + statusString = 'Disconnected'; + break; + } + return InfoTile( + showMessage: showStatus, + tileAnchor: Alignment.topCenter, + childAnchor: Alignment.topCenter, + message: statusString, + child: StreamChannel( + showLoading: false, + channel: channel, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ChipsInputTextField( + key: _chipInputTextFieldStateKey, + controller: _controller, + focusNode: _searchFocusNode, + chipBuilder: (context, user) { + return GestureDetector( + onTap: () { + _chipInputTextFieldState.removeItem(user); + _searchFocusNode.requestFocus(); }, - pagination: PaginationParams( - limit: 25, - ), - filter: { - if (_userNameQuery.isNotEmpty) - 'name': { - r'$autocomplete': _userNameQuery, - }, - 'id': { - r'$ne': StreamChat.of(context).user.id, - }, - }, - sort: [ - SortOption( - 'name', - direction: 1, - ), - ], - emptyBuilder: (_) { - return LayoutBuilder( - builder: (context, viewportConstraints) { - return SingleChildScrollView( - physics: AlwaysScrollableScrollPhysics(), - child: ConstrainedBox( - constraints: BoxConstraints( - minHeight: viewportConstraints.maxHeight, + child: Stack( + alignment: AlignmentDirectional.centerStart, + children: [ + Container( + decoration: BoxDecoration( + color: StreamChatTheme.of(context) + .colorTheme + .greyGainsboro, + borderRadius: BorderRadius.circular(12), + ), + padding: const EdgeInsets.only(left: 24), + child: Padding( + padding: + const EdgeInsets.fromLTRB(8, 4, 12, 4), + child: Text( + user.name, + maxLines: 1, + style: TextStyle( + color: StreamChatTheme.of(context) + .colorTheme + .black, ), - child: Center( - child: Column( - children: [ - Padding( - padding: const EdgeInsets.all(24), - child: StreamSvgIcon.search( - size: 96, - color: Colors.grey, + ), + ), + ), + Container( + foregroundDecoration: BoxDecoration( + color: StreamChatTheme.of(context) + .colorTheme + .overlay, + shape: BoxShape.circle, + ), + child: UserAvatar( + showOnlineStatus: false, + user: user, + constraints: BoxConstraints.tightFor( + height: 24, + width: 24, + ), + ), + ), + StreamSvgIcon.close(), + ], + ), + ); + }, + onChipAdded: (user) { + setState(() => _selectedUsers.add(user)); + }, + onChipRemoved: (user) { + setState(() => _selectedUsers.remove(user)); + }, + ), + if (!_isSearchActive && !_selectedUsers.isNotEmpty) + Container( + child: InkWell( + onTap: () { + Navigator.pushNamed( + context, + Routes.NEW_GROUP_CHAT, + ); + }, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 8), + child: Row( + children: [ + StreamNeumorphicButton( + child: Center( + child: StreamSvgIcon.contacts( + color: StreamChatTheme.of(context) + .colorTheme + .accentBlue, + size: 24, + ), + ), + ), + SizedBox(width: 8), + Text( + 'Create a Group', + style: StreamChatTheme.of(context) + .textTheme + .bodyBold, + ), + ], + ), + ), + ), + ), + if (_showUserList) + Container( + width: double.maxFinite, + decoration: BoxDecoration( + gradient: + StreamChatTheme.of(context).colorTheme.bgGradient, + ), + child: Padding( + padding: const EdgeInsets.symmetric( + vertical: 8, + horizontal: 8, + ), + child: Text( + _isSearchActive + ? "Matches for \"$_userNameQuery\"" + : 'On the platform', + style: StreamChatTheme.of(context) + .textTheme + .footnote + .copyWith( + color: StreamChatTheme.of(context) + .colorTheme + .black + .withOpacity(.5))), + ), + ), + Expanded( + child: _showUserList + ? GestureDetector( + behavior: HitTestBehavior.opaque, + onPanDown: (_) => + FocusScope.of(context).unfocus(), + child: UsersBloc( + child: UserListView( + selectedUsers: _selectedUsers, + groupAlphabetically: + _isSearchActive ? false : true, + onUserTap: (user, _) { + _controller.clear(); + if (!_selectedUsers.contains(user)) { + _chipInputTextFieldState + ..addItem(user) + ..pauseItemAddition(); + } else { + _chipInputTextFieldState.removeItem(user); + } + }, + pagination: PaginationParams( + limit: 25, + ), + filter: { + if (_userNameQuery.isNotEmpty) + 'name': { + r'$autocomplete': _userNameQuery, + }, + 'id': { + r'$ne': StreamChat.of(context).user.id, + }, + }, + sort: [ + SortOption( + 'name', + direction: 1, + ), + ], + emptyBuilder: (_) { + return LayoutBuilder( + builder: (context, viewportConstraints) { + return SingleChildScrollView( + physics: + AlwaysScrollableScrollPhysics(), + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: + viewportConstraints.maxHeight, + ), + child: Center( + child: Column( + children: [ + Padding( + padding: + const EdgeInsets.all( + 24), + child: StreamSvgIcon.search( + size: 96, + color: Colors.grey, + ), + ), + Text( + 'No user matches these keywords...', + style: StreamChatTheme.of( + context) + .textTheme + .footnote + .copyWith( + color: StreamChatTheme + .of(context) + .colorTheme + .black + .withOpacity( + .5)), + ), + ], + ), ), ), - Text( - 'No user matches these keywords...', - style: StreamChatTheme.of(context) - .textTheme - .footnote - .copyWith( - color: StreamChatTheme.of( - context) - .colorTheme - .black - .withOpacity(.5)), - ), - ], - ), + ); + }, + ); + }, + ), + ), + ) + : FutureBuilder( + future: channel.initialized, + builder: (context, snapshot) { + if (snapshot.data == true) { + return MessageListView(); + } + + return Center( + child: Text( + 'No chats here yet...', + style: TextStyle( + fontSize: 12, + color: StreamChatTheme.of(context) + .colorTheme + .black + .withOpacity(.5), ), ), ); }, - ); - }, - ), - ), - ) - : FutureBuilder( - future: channel.initialized, - builder: (context, snapshot) { - if (snapshot.data == true) { - return MessageListView(); - } - - return Center( - child: Text( - 'No chats here yet...', - style: TextStyle( - fontSize: 12, - color: StreamChatTheme.of(context) - .colorTheme - .black - .withOpacity(.5), ), - ), + ), + MessageInput( + focusNode: _messageInputFocusNode, + preMessageSending: (message) async { + await channel.watch(); + return message; + }, + onMessageSent: (m) { + Navigator.pushNamedAndRemoveUntil( + context, + Routes.CHANNEL_PAGE, + ModalRoute.withName(Routes.HOME), + arguments: ChannelPageArgs(channel: channel), ); }, ), - ), - MessageInput( - focusNode: _messageInputFocusNode, - preMessageSending: (message) async { - await channel.watch(); - return message; - }, - onMessageSent: (m) { - Navigator.pushNamedAndRemoveUntil( - context, - Routes.CHANNEL_PAGE, - ModalRoute.withName(Routes.HOME), - arguments: ChannelPageArgs(channel: channel), - ); - }, - ), - ], - ), - ), + ], + ), + ), + ); + }), ); } } diff --git a/lib/src/message_list_view.dart b/lib/src/message_list_view.dart index 7cd4ad28..f60b9f5e 100644 --- a/lib/src/message_list_view.dart +++ b/lib/src/message_list_view.dart @@ -307,164 +307,198 @@ class _MessageListViewState extends State { return Stack( alignment: Alignment.center, children: [ - LazyLoadScrollView( - 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, - ), - ), - ); + ValueListenableBuilder( + valueListenable: _client.wsConnectionStatus, + builder: (context, status, _) { + String statusString = ''; + bool showStatus = true; + + switch (status) { + case ConnectionStatus.connected: + statusString = 'Connected'; + showStatus = false; + break; + case ConnectionStatus.connecting: + statusString = 'Reconnecting...'; + break; + case ConnectionStatus.disconnected: + statusString = 'Disconnected'; + break; } - 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 InfoTile( + showMessage: + widget.showConnectionStateTile ? showStatus : false, + tileAnchor: Alignment.topCenter, + childAnchor: Alignment.topCenter, + message: statusString, + child: LazyLoadScrollView( + child: LazyLoadScrollView( + 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, ); - return Padding( - padding: const EdgeInsets.symmetric(vertical: 12.0), - child: divider, - ); - } - final timeDiff = - Jiffy(nextMessage.createdAt.toLocal()).diff( - message.createdAt.toLocal(), - Units.MINUTE, - ); + }, + 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 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, + 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, ); - } 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), + + 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 { - messageWidget = - buildMessage(message, messages, i); + 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); + } } - } - return messageWidget; - }, + return messageWidget; + }, + ), ), ), ); From 1fd81adbf43460dfab156a5ec16378e309d01748 Mon Sep 17 00:00:00 2001 From: Deven Joshi Date: Wed, 13 Jan 2021 16:09:23 +0530 Subject: [PATCH 27/28] feat: Added info tile to group screens --- example/lib/group_chat_details_screen.dart | 179 ++++++---- example/lib/new_group_chat_screen.dart | 389 +++++++++++---------- 2 files changed, 315 insertions(+), 253 deletions(-) diff --git a/example/lib/group_chat_details_screen.dart b/example/lib/group_chat_details_screen.dart index f59b5c4f..83ec94bb 100644 --- a/example/lib/group_chat_details_screen.dart +++ b/example/lib/group_chat_details_screen.dart @@ -150,85 +150,116 @@ class _GroupChatDetailsScreenState extends State { ), ], ), - body: Column( - children: [ - Container( - width: double.maxFinite, - decoration: BoxDecoration( - gradient: StreamChatTheme.of(context).colorTheme.bgGradient, - ), - child: Padding( - padding: const EdgeInsets.symmetric( - vertical: 8, - horizontal: 8, - ), - child: Text( - '$_totalUsers ${_totalUsers > 1 ? 'Members' : 'Member'}', - style: TextStyle( - color: StreamChatTheme.of(context).colorTheme.grey, - ), - ), - ), - ), - Expanded( - child: GestureDetector( - behavior: HitTestBehavior.opaque, - onPanDown: (_) => FocusScope.of(context).unfocus(), - child: ListView.separated( - itemCount: _selectedUsers.length + 1, - separatorBuilder: (_, __) => Container( - height: 1, - color: StreamChatTheme.of(context).colorTheme.greyWhisper, - ), - itemBuilder: (_, index) { - if (index == _selectedUsers.length) { - return Container( - height: 1, - color: - StreamChatTheme.of(context).colorTheme.greyWhisper, - ); - } - final user = _selectedUsers[index]; - return ListTile( - key: ObjectKey(user), - leading: UserAvatar( - user: user, - constraints: BoxConstraints.tightFor( - width: 40, - height: 40, + body: ValueListenableBuilder( + valueListenable: StreamChat.of(context).client.wsConnectionStatus, + builder: (context, status, _) { + String statusString = ''; + bool showStatus = true; + + switch (status) { + case ConnectionStatus.connected: + statusString = 'Connected'; + showStatus = false; + break; + case ConnectionStatus.connecting: + statusString = 'Reconnecting...'; + break; + case ConnectionStatus.disconnected: + statusString = 'Disconnected'; + break; + } + return InfoTile( + showMessage: showStatus, + tileAnchor: Alignment.topCenter, + childAnchor: Alignment.topCenter, + message: statusString, + child: Column( + children: [ + Container( + width: double.maxFinite, + decoration: BoxDecoration( + gradient: + StreamChatTheme.of(context).colorTheme.bgGradient, + ), + child: Padding( + padding: const EdgeInsets.symmetric( + vertical: 8, + horizontal: 8, + ), + child: Text( + '$_totalUsers ${_totalUsers > 1 ? 'Members' : 'Member'}', + style: TextStyle( + color: StreamChatTheme.of(context).colorTheme.grey, + ), ), ), - title: Text( - user.name, - style: TextStyle(fontWeight: FontWeight.bold), - ), - contentPadding: const EdgeInsets.symmetric( - horizontal: 12, - vertical: 8, - ), - trailing: IconButton( - icon: Icon( - Icons.clear_rounded, - color: StreamChatTheme.of(context).colorTheme.black, + ), + Expanded( + child: GestureDetector( + behavior: HitTestBehavior.opaque, + onPanDown: (_) => FocusScope.of(context).unfocus(), + child: ListView.separated( + itemCount: _selectedUsers.length + 1, + separatorBuilder: (_, __) => Container( + height: 1, + color: StreamChatTheme.of(context) + .colorTheme + .greyWhisper, + ), + itemBuilder: (_, index) { + if (index == _selectedUsers.length) { + return Container( + height: 1, + color: StreamChatTheme.of(context) + .colorTheme + .greyWhisper, + ); + } + final user = _selectedUsers[index]; + return ListTile( + key: ObjectKey(user), + leading: UserAvatar( + user: user, + constraints: BoxConstraints.tightFor( + width: 40, + height: 40, + ), + ), + title: Text( + user.name, + style: TextStyle(fontWeight: FontWeight.bold), + ), + contentPadding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 8, + ), + trailing: IconButton( + icon: Icon( + Icons.clear_rounded, + color: StreamChatTheme.of(context) + .colorTheme + .black, + ), + padding: const EdgeInsets.all(0), + splashRadius: 24, + onPressed: () { + setState(() { + _selectedUsers.remove(user); + }); + if (_selectedUsers.isEmpty) { + Navigator.pop(context, _selectedUsers); + } + }, + ), + ); + }, ), - padding: const EdgeInsets.all(0), - splashRadius: 24, - onPressed: () { - setState(() { - _selectedUsers.remove(user); - }); - if (_selectedUsers.isEmpty) { - Navigator.pop(context, _selectedUsers); - } - }, ), - ); - }, + ), + ], ), - ), - ), - ], - ), + ); + }), ), ); } diff --git a/example/lib/new_group_chat_screen.dart b/example/lib/new_group_chat_screen.dart index ceb91473..4d0274e2 100644 --- a/example/lib/new_group_chat_screen.dart +++ b/example/lib/new_group_chat_screen.dart @@ -87,196 +87,227 @@ class _NewGroupChatScreenState extends State { ) ], ), - body: NestedScrollView( - floatHeaderSlivers: true, - headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) { - return [ - SliverToBoxAdapter( - child: SearchTextField( - controller: _controller, - ), - ), - if (_selectedUsers.isNotEmpty) - SliverToBoxAdapter( - child: Container( - height: 104, - child: ListView.separated( - scrollDirection: Axis.horizontal, - itemCount: _selectedUsers.length, - padding: const EdgeInsets.all(8), - separatorBuilder: (_, __) => SizedBox(width: 16), - itemBuilder: (_, index) { - final user = _selectedUsers.elementAt(index); - return Column( - children: [ - Stack( - children: [ - UserAvatar( - onlineIndicatorAlignment: Alignment(0.9, 0.9), - user: user, - showOnlineStatus: true, - borderRadius: BorderRadius.circular(32), - constraints: BoxConstraints.tightFor( - height: 64, - width: 64, - ), - ), - Positioned( - top: -4, - right: -4, - child: GestureDetector( - onTap: () { - if (_selectedUsers.contains(user)) { - setState( - () => _selectedUsers.remove(user)); - } - }, - child: Container( - decoration: BoxDecoration( - color: StreamChatTheme.of(context) - .colorTheme - .white, - shape: BoxShape.circle, - border: Border.all( - color: StreamChatTheme.of(context) - .colorTheme - .whiteSnow, + body: ValueListenableBuilder( + valueListenable: StreamChat.of(context).client.wsConnectionStatus, + builder: (context, status, _) { + String statusString = ''; + bool showStatus = true; + + switch (status) { + case ConnectionStatus.connected: + statusString = 'Connected'; + showStatus = false; + break; + case ConnectionStatus.connecting: + statusString = 'Reconnecting...'; + break; + case ConnectionStatus.disconnected: + statusString = 'Disconnected'; + break; + } + return InfoTile( + showMessage: showStatus, + tileAnchor: Alignment.topCenter, + childAnchor: Alignment.topCenter, + message: statusString, + child: NestedScrollView( + floatHeaderSlivers: true, + headerSliverBuilder: + (BuildContext context, bool innerBoxIsScrolled) { + return [ + SliverToBoxAdapter( + child: SearchTextField( + controller: _controller, + ), + ), + if (_selectedUsers.isNotEmpty) + SliverToBoxAdapter( + child: Container( + height: 104, + child: ListView.separated( + scrollDirection: Axis.horizontal, + itemCount: _selectedUsers.length, + padding: const EdgeInsets.all(8), + separatorBuilder: (_, __) => SizedBox(width: 16), + itemBuilder: (_, index) { + final user = _selectedUsers.elementAt(index); + return Column( + children: [ + Stack( + children: [ + UserAvatar( + onlineIndicatorAlignment: + Alignment(0.9, 0.9), + user: user, + showOnlineStatus: true, + borderRadius: BorderRadius.circular(32), + constraints: BoxConstraints.tightFor( + height: 64, + width: 64, + ), ), - ), - child: StreamSvgIcon.close( - color: StreamChatTheme.of(context) - .colorTheme - .black, - size: 24, + Positioned( + top: -4, + right: -4, + child: GestureDetector( + onTap: () { + if (_selectedUsers.contains(user)) { + setState(() => + _selectedUsers.remove(user)); + } + }, + child: Container( + decoration: BoxDecoration( + color: StreamChatTheme.of(context) + .colorTheme + .white, + shape: BoxShape.circle, + border: Border.all( + color: + StreamChatTheme.of(context) + .colorTheme + .whiteSnow, + ), + ), + child: StreamSvgIcon.close( + color: StreamChatTheme.of(context) + .colorTheme + .black, + size: 24, + ), + ), + ), + ) + ], + ), + SizedBox(height: 4), + Text( + user.name.split(' ')[0], + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 12, ), ), - ), - ) - ], + ], + ); + }, ), - SizedBox(height: 4), - Text( - user.name.split(' ')[0], - style: TextStyle( - fontWeight: FontWeight.bold, - fontSize: 12, + ), + ), + SliverPersistentHeader( + pinned: true, + delegate: _HeaderDelegate( + height: 30, + child: Container( + width: double.maxFinite, + decoration: BoxDecoration( + gradient: StreamChatTheme.of(context) + .colorTheme + .bgGradient, + ), + child: Padding( + padding: const EdgeInsets.symmetric( + vertical: 8, + horizontal: 8, + ), + child: Text( + _isSearchActive + ? 'Matches for \"$_userNameQuery\"' + : 'On the platform', + style: TextStyle( + color: + StreamChatTheme.of(context).colorTheme.grey, + ), ), ), - ], - ); - }, - ), - ), - ), - SliverPersistentHeader( - pinned: true, - delegate: _HeaderDelegate( - height: 30, - child: Container( - width: double.maxFinite, - decoration: BoxDecoration( - gradient: StreamChatTheme.of(context).colorTheme.bgGradient, - ), - child: Padding( - padding: const EdgeInsets.symmetric( - vertical: 8, - horizontal: 8, - ), - child: Text( - _isSearchActive - ? 'Matches for \"$_userNameQuery\"' - : 'On the platform', - style: TextStyle( - color: StreamChatTheme.of(context).colorTheme.grey, + ), ), ), - ), - ), - ), - ), - ]; - }, - body: GestureDetector( - behavior: HitTestBehavior.opaque, - onPanDown: (_) => FocusScope.of(context).unfocus(), - child: UsersBloc( - child: UserListView( - selectedUsers: _selectedUsers, - pullToRefresh: false, - groupAlphabetically: _isSearchActive ? false : true, - onUserTap: (user, _) { - if (!_selectedUsers.contains(user)) { - setState(() { - _selectedUsers.add(user); - }); - } else { - setState(() { - _selectedUsers.remove(user); - }); - } - }, - pagination: PaginationParams( - limit: 25, - ), - filter: { - if (_userNameQuery.isNotEmpty) - 'name': { - r'$autocomplete': _userNameQuery, - }, - 'id': { - r'$ne': StreamChat.of(context).user.id, - } - }, - sort: [ - SortOption( - 'name', - direction: 1, - ), - ], - emptyBuilder: (_) { - return LayoutBuilder( - builder: (context, viewportConstraints) { - return SingleChildScrollView( - physics: AlwaysScrollableScrollPhysics(), - child: ConstrainedBox( - constraints: BoxConstraints( - minHeight: viewportConstraints.maxHeight, + ]; + }, + body: GestureDetector( + behavior: HitTestBehavior.opaque, + onPanDown: (_) => FocusScope.of(context).unfocus(), + child: UsersBloc( + child: UserListView( + selectedUsers: _selectedUsers, + pullToRefresh: false, + groupAlphabetically: _isSearchActive ? false : true, + onUserTap: (user, _) { + if (!_selectedUsers.contains(user)) { + setState(() { + _selectedUsers.add(user); + }); + } else { + setState(() { + _selectedUsers.remove(user); + }); + } + }, + pagination: PaginationParams( + limit: 25, + ), + filter: { + if (_userNameQuery.isNotEmpty) + 'name': { + r'$autocomplete': _userNameQuery, + }, + 'id': { + r'$ne': StreamChat.of(context).user.id, + } + }, + sort: [ + SortOption( + 'name', + direction: 1, ), - child: Center( - child: Column( - children: [ - Padding( - padding: const EdgeInsets.all(24), - child: StreamSvgIcon.search( - size: 96, - color: StreamChatTheme.of(context) - .colorTheme - .grey, + ], + emptyBuilder: (_) { + return LayoutBuilder( + builder: (context, viewportConstraints) { + return SingleChildScrollView( + physics: AlwaysScrollableScrollPhysics(), + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: viewportConstraints.maxHeight, + ), + child: Center( + child: Column( + children: [ + Padding( + padding: const EdgeInsets.all(24), + child: StreamSvgIcon.search( + size: 96, + color: StreamChatTheme.of(context) + .colorTheme + .grey, + ), + ), + Text( + 'No user matches these keywords...', + style: StreamChatTheme.of(context) + .textTheme + .footnote + .copyWith( + color: StreamChatTheme.of(context) + .colorTheme + .grey, + ), + ), + ], + ), ), ), - Text( - 'No user matches these keywords...', - style: StreamChatTheme.of(context) - .textTheme - .footnote - .copyWith( - color: StreamChatTheme.of(context) - .colorTheme - .grey, - ), - ), - ], - ), - ), - ), - ); - }, - ); - }, - ), - ), - ), - ), + ); + }, + ); + }, + ), + ), + ), + ), + ); + }), ); } } From 9cba3d6f2b11f8e6feb4a043c53898543e82ecb4 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Wed, 13 Jan 2021 12:07:39 +0100 Subject: [PATCH 28/28] update version --- example/pubspec.yaml | 2 +- pubspec.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/example/pubspec.yaml b/example/pubspec.yaml index 2b8859ea..941d8bcd 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -1,6 +1,6 @@ name: example description: A new Flutter project. -version: 1.1.1+1 +version: 1.1.1+2 environment: sdk: ">=2.2.2 <3.0.0" diff --git a/pubspec.yaml b/pubspec.yaml index e29042cb..9ef7a80a 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -28,7 +28,7 @@ dependencies: file_picker: ^2.0.12 image_picker: ^0.6.7+2 flutter_keyboard_visibility: ^4.0.2 - stream_chat: ^0.2.23 + stream_chat: ^0.2.23+2 mime: ^0.9.6+3 video_compress: ^2.1.1 visibility_detector: ^0.1.5