diff --git a/packages/flutter_widgets/example/lib/new_chat_screen.dart b/packages/flutter_widgets/example/lib/new_chat_screen.dart index 5772f5e2..baddd570 100644 --- a/packages/flutter_widgets/example/lib/new_chat_screen.dart +++ b/packages/flutter_widgets/example/lib/new_chat_screen.dart @@ -135,287 +135,284 @@ class _NewChatScreenState extends State { centerTitle: true, ), body: ValueListenableBuilder( - valueListenable: StreamChat.of(context).client.wsConnectionStatus, - builder: (context, status, _) { - String statusString = ''; - bool showStatus = true; + 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(); - }, - child: Stack( - alignment: AlignmentDirectional.centerStart, + 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(); + }, + 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: [ - 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, - ), + StreamNeumorphicButton( + child: Center( + child: StreamSvgIcon.contacts( + color: StreamChatTheme.of(context) + .colorTheme + .accentBlue, + size: 24, ), ), ), - 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, - ), - ), + SizedBox(width: 8), + Text( + 'Create a Group', + style: StreamChatTheme.of(context) + .textTheme + .bodyBold, ), - 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, ), - 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))), + 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, + ), + 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, - ), + }, + 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), - ), - ), - ); - }, ), - ), - 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), - ); - }, - ), - ], - ), + ) + : 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), + ); + }, + ), + ], ), - ); - }), + ), + ); + }, + ), ); } } diff --git a/packages/flutter_widgets/lib/src/channel_list_view.dart b/packages/flutter_widgets/lib/src/channel_list_view.dart index 617b7ce9..84eb1298 100644 --- a/packages/flutter_widgets/lib/src/channel_list_view.dart +++ b/packages/flutter_widgets/lib/src/channel_list_view.dart @@ -159,34 +159,36 @@ class _ChannelListViewState extends State @override Widget build(BuildContext context) { - if (!widget.pullToRefresh) { - _channelListController.loadData(); - } - - return RefreshIndicator( - onRefresh: () async { - _channelListController.loadData(); + var child = ChannelListCore( + channelListController: _channelListController, + listBuilder: (context, list) { + return _buildListView(list); }, - child: ChannelListCore( - channelListController: _channelListController, - listBuilder: (context, list) { - return _buildListView(list); - }, - emptyBuilder: (BuildContext context) { - return _buildEmptyWidget(); - }, - errorBuilder: (Error error) { - return _buildErrorWidget(context); - }, - loadingBuilder: (BuildContext context) { - return _buildLoadingWidget(); - }, - pagination: widget.pagination, - options: widget.options, - sort: widget.sort, - filter: widget.filter, - ), + emptyBuilder: (BuildContext context) { + return _buildEmptyWidget(); + }, + errorBuilder: (Error error) { + return _buildErrorWidget(context); + }, + loadingBuilder: (BuildContext context) { + return _buildLoadingWidget(); + }, + pagination: widget.pagination, + options: widget.options, + sort: widget.sort, + filter: widget.filter, ); + + if (!widget.pullToRefresh) { + return child; + } else { + return RefreshIndicator( + onRefresh: () async { + _channelListController.loadData(); + }, + child: child, + ); + } } Widget _buildListView( diff --git a/packages/flutter_widgets/lib/src/user_list_view.dart b/packages/flutter_widgets/lib/src/user_list_view.dart index be34d3db..7027a875 100644 --- a/packages/flutter_widgets/lib/src/user_list_view.dart +++ b/packages/flutter_widgets/lib/src/user_list_view.dart @@ -140,209 +140,170 @@ class _UserListViewState extends State with WidgetsBindingObserver { bool get _isListView => widget.crossAxisCount == 1; - @override - void initState() { - super.initState(); - final usersBloc = UsersBloc.of(context); - usersBloc.queryUsers( - filter: widget.filter, - sort: widget.sort, - pagination: widget.pagination, - options: widget.options, - ); - } + UserListController _userListController = UserListController(); @override Widget build(BuildContext context) { - final usersBloc = UsersBloc.of(context); - - if (!widget.pullToRefresh) { - return _buildListView(usersBloc); - } - - return RefreshIndicator( - onRefresh: () async { - return usersBloc.queryUsers( - filter: widget.filter, - sort: widget.sort, - options: widget.options, - pagination: widget.pagination, + var child = UserListCore( + errorBuilder: (err) { + return _buildError(err); + }, + emptyBuilder: (context) { + return _buildEmpty(); + }, + loadingBuilder: (context) { + return LayoutBuilder( + builder: (context, viewportConstraints) { + return SingleChildScrollView( + physics: AlwaysScrollableScrollPhysics(), + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: viewportConstraints.maxHeight, + ), + child: Center( + child: CircularProgressIndicator(), + ), + ), + ); + }, ); }, - child: _buildListView(usersBloc), + listBuilder: (context, list) { + return _buildListView(list); + }, + pagination: widget.pagination, + options: widget.options, + sort: widget.sort, + filter: widget.filter, + groupAlphabetically: widget.groupAlphabetically, + userListController: _userListController, ); + + if (!widget.pullToRefresh) { + return child; + } else { + return RefreshIndicator( + onRefresh: () async { + _userListController.loadData(); + }, + child: child, + ); + } } bool get isListAlreadySorted => widget.sort?.any((e) => e.field == 'name' && e.direction == 1) ?? false; - Stream> _buildUserStream( - UsersBlocState usersBlocState, - ) { - return usersBlocState.usersStream.map( - (users) { - if (widget.groupAlphabetically) { - var temp = users; - if (!isListAlreadySorted) { - temp = users..sort((curr, next) => curr.name.compareTo(next.name)); - } - final groupedUsers = >{}; - for (var e in temp) { - final alphabet = e.name[0]?.toUpperCase(); - groupedUsers[alphabet] = [...groupedUsers[alphabet] ?? [], e]; - } - final items = []; - for (var key in groupedUsers.keys) { - items.add(ListHeaderItem(key)); - items.addAll(groupedUsers[key].map((e) => ListUserItem(e))); - } - return items; - } - return users.map((e) => ListUserItem(e)).toList(); - }, + Widget _buildError(Error error) { + print((error).stackTrace); + + if (widget.errorBuilder != null) { + return widget.errorBuilder(error); + } + + var message = error.toString(); + if (error is DioError) { + final dioError = error as DioError; + if (dioError.type == DioErrorType.RESPONSE) { + message = dioError.message; + } else { + 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), + ), + ), + TextSpan(text: 'Error loading channels'), + ], + ), + style: Theme.of(context).textTheme.headline6, + ), + Padding( + padding: const EdgeInsets.only( + top: 16.0, + ), + child: Text(message), + ), + FlatButton( + onPressed: () { + _userListController.loadData(); + }, + child: Text('Retry'), + ), + ], + ), ); } - StreamBuilder> _buildListView( - UsersBlocState usersBlocState, - ) { - return StreamBuilder( - stream: _buildUserStream(usersBlocState), - builder: (context, snapshot) { - if (snapshot.hasError) { - if (snapshot.error is Error) { - print((snapshot.error as Error).stackTrace); - } + Widget _buildEmpty() { + if (widget.emptyBuilder != null) { + return widget.emptyBuilder(context); + } - if (widget.errorBuilder != null) { - return widget.errorBuilder(snapshot.error); - } - - var message = snapshot.error.toString(); - if (snapshot.error is DioError) { - final dioError = snapshot.error as DioError; - if (dioError.type == DioErrorType.RESPONSE) { - message = dioError.message; - } else { - 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), - ), - ), - TextSpan(text: 'Error loading channels'), - ], - ), - style: Theme.of(context).textTheme.headline6, - ), - Padding( - padding: const EdgeInsets.only( - top: 16.0, - ), - child: Text(message), - ), - FlatButton( - onPressed: () { - usersBlocState.queryUsers( - filter: widget.filter, - sort: widget.sort, - pagination: widget.pagination, - options: widget.options, - ); - }, - child: Text('Retry'), - ), - ], + if (widget.emptyBuilder == null) { + return LayoutBuilder( + builder: (context, viewportConstraints) { + return SingleChildScrollView( + physics: AlwaysScrollableScrollPhysics(), + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: viewportConstraints.maxHeight, + ), + child: Center( + child: Text('There are no users currently'), + ), ), ); - } + }, + ); + } + } - if (!snapshot.hasData) { - return LayoutBuilder( - builder: (context, viewportConstraints) { - return SingleChildScrollView( - physics: AlwaysScrollableScrollPhysics(), - child: ConstrainedBox( - constraints: BoxConstraints( - minHeight: viewportConstraints.maxHeight, - ), - child: Center( - child: CircularProgressIndicator(), - ), - ), - ); + Widget _buildListView( + List items, + ) { + final child = _isListView + ? ListView.separated( + physics: AlwaysScrollableScrollPhysics(), + itemCount: items.isNotEmpty ? items.length + 1 : items.length, + separatorBuilder: (_, index) { + if (widget.separatorBuilder != null) { + return widget.separatorBuilder(context, index); + } + return _separatorBuilder(context, index); + }, + itemBuilder: (context, index) { + return _listItemBuilder(context, index, items); + }, + ) + : GridView.builder( + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: widget.crossAxisCount, + ), + itemCount: items.isNotEmpty ? items.length + 1 : items.length, + physics: AlwaysScrollableScrollPhysics(), + itemBuilder: (context, index) { + return _gridItemBuilder(context, index, items); }, ); - } - final items = snapshot.data; - - if (items.isEmpty && widget.emptyBuilder != null) { - return widget.emptyBuilder(context); - } - - if (items.isEmpty && widget.emptyBuilder == null) { - return LayoutBuilder( - builder: (context, viewportConstraints) { - return SingleChildScrollView( - physics: AlwaysScrollableScrollPhysics(), - child: ConstrainedBox( - constraints: BoxConstraints( - minHeight: viewportConstraints.maxHeight, - ), - child: Center( - child: Text('There are no users currently'), - ), - ), - ); - }, - ); - } - - final child = _isListView - ? ListView.separated( - physics: AlwaysScrollableScrollPhysics(), - itemCount: items.isNotEmpty ? items.length + 1 : items.length, - separatorBuilder: (_, index) { - if (widget.separatorBuilder != null) { - return widget.separatorBuilder(context, index); - } - return _separatorBuilder(context, index); - }, - itemBuilder: (context, index) { - return _listItemBuilder(context, index, items); - }, - ) - : GridView.builder( - gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: widget.crossAxisCount, - ), - itemCount: items.isNotEmpty ? items.length + 1 : items.length, - physics: AlwaysScrollableScrollPhysics(), - itemBuilder: (context, index) { - return _gridItemBuilder(context, index, items); - }, - ); - - return LazyLoadScrollView( - onEndOfPage: () async { - return _listenUserPagination(usersBlocState); - }, - child: child, - ); + return LazyLoadScrollView( + onEndOfPage: () async { + return _userListController.paginateData(); }, + child: child, ); } @@ -481,33 +442,4 @@ class _UserListViewState extends State color: StreamChatTheme.of(context).colorTheme.greyWhisper, ); } - - void _listenUserPagination(UsersBlocState usersProvider) { - usersProvider.queryUsers( - filter: widget.filter, - sort: widget.sort, - pagination: widget.pagination.copyWith( - offset: usersProvider.users?.length ?? 0, - ), - options: widget.options, - ); - } - - @override - void didUpdateWidget(UserListView oldWidget) { - super.didUpdateWidget(oldWidget); - if (widget.filter?.toString() != oldWidget.filter?.toString() || - jsonEncode(widget.sort) != jsonEncode(oldWidget.sort) || - widget.pagination?.toJson()?.toString() != - oldWidget.pagination?.toJson()?.toString() || - widget.options?.toString() != oldWidget.options?.toString()) { - final usersBloc = UsersBloc.of(context); - usersBloc.queryUsers( - filter: widget.filter, - sort: widget.sort, - pagination: widget.pagination, - options: widget.options, - ); - } - } } diff --git a/packages/stream_chat_flutter_core/lib/src/user_list_core.dart b/packages/stream_chat_flutter_core/lib/src/user_list_core.dart index 8edf8752..bb8f81fe 100644 --- a/packages/stream_chat_flutter_core/lib/src/user_list_core.dart +++ b/packages/stream_chat_flutter_core/lib/src/user_list_core.dart @@ -43,7 +43,6 @@ class UserListCore extends StatefulWidget { this.options, this.sort, this.pagination, - this.pullToRefresh = true, this.groupAlphabetically = false, @required this.errorBuilder, @required this.emptyBuilder, @@ -89,9 +88,6 @@ class UserListCore extends StatefulWidget { /// message_limit: how many messages should be included to each channel final PaginationParams pagination; - /// Set it to false to disable the pull-to-refresh widget - final bool pullToRefresh; - /// Set it to true to group users by their first character /// /// defaults to false @@ -124,21 +120,7 @@ class _UserListCoreState extends State Widget build(BuildContext context) { final usersBloc = UsersBloc.of(context); - if (!widget.pullToRefresh) { - return _buildListView(usersBloc); - } - - return RefreshIndicator( - onRefresh: () async { - return usersBloc.queryUsers( - filter: widget.filter, - sort: widget.sort, - options: widget.options, - pagination: widget.pagination, - ); - }, - child: _buildListView(usersBloc), - ); + return _buildListView(usersBloc); } bool get isListAlreadySorted =>