From f04452cbc9cfa27c8a6b8d37c47209b56fb0e8dd Mon Sep 17 00:00:00 2001 From: Deven Joshi Date: Tue, 12 Jan 2021 18:04:39 +0530 Subject: [PATCH 01/10] 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 53587a86553342db4d93133ac79d9d71f28f2e07 Mon Sep 17 00:00:00 2001 From: Deven Joshi Date: Tue, 12 Jan 2021 18:14:00 +0530 Subject: [PATCH 02/10] 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 03/10] 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 04/10] 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 fad38a513ac0a50ca521363af74132063a618044 Mon Sep 17 00:00:00 2001 From: Deven Joshi Date: Tue, 12 Jan 2021 21:13:38 +0530 Subject: [PATCH 05/10] 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 06/10] 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 f3ff7588946e8eae6c2a0defa789b4171ef949d4 Mon Sep 17 00:00:00 2001 From: Deven Joshi Date: Wed, 13 Jan 2021 13:14:36 +0530 Subject: [PATCH 07/10] 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 08/10] 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 09/10] 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 From aaf2d3947304d6fe72701cec1f31f2763059d4db Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Wed, 13 Jan 2021 13:47:59 +0100 Subject: [PATCH 10/10] add quoted messages to threads --- example/lib/main.dart | 43 ++++++++++++++++++++++++++++++++++++------- 1 file changed, 36 insertions(+), 7 deletions(-) diff --git a/example/lib/main.dart b/example/lib/main.dart index 49ee3fdc..7b2f578a 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -763,7 +763,7 @@ class _ChannelPageState extends State { } } -class ThreadPage extends StatelessWidget { +class ThreadPage extends StatefulWidget { final Message parent; final int initialScrollIndex; final double initialAlignment; @@ -775,25 +775,54 @@ class ThreadPage extends StatelessWidget { this.initialAlignment, }) : super(key: key); + @override + _ThreadPageState createState() => _ThreadPageState(); +} + +class _ThreadPageState extends State { + Message _quotedMessage; + FocusNode _focusNode = FocusNode(); + + @override + void dispose() { + _focusNode.dispose(); + super.dispose(); + } + + void _reply(Message message) { + setState(() => _quotedMessage = message); + WidgetsBinding.instance.addPostFrameCallback((timeStamp) { + _focusNode.requestFocus(); + }); + } + @override Widget build(BuildContext context) { return Scaffold( backgroundColor: StreamChatTheme.of(context).colorTheme.whiteSnow, appBar: ThreadHeader( - parent: parent, + parent: widget.parent, ), body: Column( children: [ Expanded( child: MessageListView( - parentMessage: parent, - initialScrollIndex: initialScrollIndex, - initialAlignment: initialAlignment, + parentMessage: widget.parent, + initialScrollIndex: widget.initialScrollIndex, + initialAlignment: widget.initialAlignment, + onMessageSwiped: _reply, + onReplyTap: _reply, ), ), - if (parent.type != 'deleted') + if (widget.parent.type != 'deleted') MessageInput( - parentMessage: parent, + parentMessage: widget.parent, + focusNode: _focusNode, + quotedMessage: _quotedMessage, + onQuotedMessageCleared: () { + setState(() => _quotedMessage = null); + _focusNode.unfocus(); + }, ), ], ),