From 413dec0336d66b5bbddefa2b385149e58770fb62 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Tue, 17 Nov 2020 16:00:52 +0530 Subject: [PATCH 01/40] feat: Navigate to new chat screen on drawer item click Signed-off-by: xsahil03x --- example/lib/main.dart | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/example/lib/main.dart b/example/lib/main.dart index 2e3f43e2..7c288eae 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -63,6 +63,13 @@ class ChannelListPage extends StatelessWidget { Widget build(BuildContext context) { final user = StreamChat.of(context).user; return Scaffold( + appBar: AppBar( + backgroundColor: Colors.white, + title: Text( + 'Stream Chat', + style: TextStyle(color: Colors.black), + ), + ), drawer: Drawer( child: SafeArea( child: Padding( @@ -97,6 +104,12 @@ class ChannelListPage extends StatelessWidget { ), ), ListTile( + onTap: () { + Navigator.push( + context, + MaterialPageRoute(builder: (_) => NewChatScreen()), + ); + }, leading: Icon(StreamIcons.edit), title: Text( 'New direct message', @@ -434,3 +447,24 @@ class _CreateChannelPageState extends State { }).whenComplete(() => loading = false); } } + +class NewChatScreen extends StatefulWidget { + @override + _NewChatScreenState createState() => _NewChatScreenState(); +} + +class _NewChatScreenState extends State { + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + backgroundColor: Colors.white, + title: Text( + 'New Chat', + style: TextStyle(color: Colors.black), + ), + ), + body: Container(), + ); + } +} From a8a5e5dfdf67d2ad3c3d422ed138f72ece0047b9 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Tue, 17 Nov 2020 18:19:33 +0530 Subject: [PATCH 02/40] [Channels Bloc] Fix typo Signed-off-by: Sahil Kumar --- lib/src/channels_bloc.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/channels_bloc.dart b/lib/src/channels_bloc.dart index 3af2300d..474d9599 100644 --- a/lib/src/channels_bloc.dart +++ b/lib/src/channels_bloc.dart @@ -31,7 +31,7 @@ class ChannelsBloc extends StatefulWidget { streamChatState = context.findAncestorStateOfType(); if (streamChatState == null) { - throw Exception('You must have a ChannelsBloc widget as anchestor'); + throw Exception('You must have a ChannelsBloc widget as ancestor'); } return streamChatState; From cd8ca90052836203a8a47bbc4049793d93fe5b38 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Tue, 17 Nov 2020 21:30:05 +0530 Subject: [PATCH 03/40] feat: user list Signed-off-by: Sahil Kumar --- lib/src/user_list_view.dart | 468 +++++++++++++++++++++++++++++++++++ lib/src/users_bloc.dart | 114 +++++++++ lib/stream_chat_flutter.dart | 1 + 3 files changed, 583 insertions(+) create mode 100644 lib/src/user_list_view.dart create mode 100644 lib/src/users_bloc.dart diff --git a/lib/src/user_list_view.dart b/lib/src/user_list_view.dart new file mode 100644 index 00000000..5b661ee8 --- /dev/null +++ b/lib/src/user_list_view.dart @@ -0,0 +1,468 @@ +import 'package:flutter/material.dart'; +import 'package:stream_chat/stream_chat.dart'; +import 'package:stream_chat_flutter/src/users_bloc.dart'; + +import 'stream_chat.dart'; + +typedef UserTapCallback = void Function(User, Widget); + +/// +/// It shows the list of current users. +/// +/// ```dart +/// class UsersListPage extends StatelessWidget { +/// @override +/// Widget build(BuildContext context) { +/// return Scaffold( +/// body: UsersListView( +/// filter: { +/// 'members': { +/// '\$in': [StreamChat.of(context).user.id], +/// } +/// }, +/// sort: [SortOption('last_message_at')], +/// pagination: PaginationParams( +/// limit: 20, +/// ), +/// channelWidget: ChannelPage(), +/// ), +/// ); +/// } +/// } +/// ``` +/// +/// +/// Make sure to have a [StreamChat] ancestor in order to provide the information about the channels. +/// The widget uses a [ListView.custom] to render the list of channels. +/// +/// The widget components render the ui based on the first ancestor of type [StreamChatTheme]. +/// Modify it to change the widget appearance. +class UserListView extends StatefulWidget { + /// Instantiate a new UserListView + const UserListView({ + Key key, + this.errorBuilder, + this.emptyBuilder, + this.filter, + this.options, + this.sort, + this.pagination, + this.onUserTap, + this.onUserLongPress, + this.userWidget, + this.separatorBuilder, + this.onImageTap, + this.swipeToAction = false, + this.pullToRefresh = true, + }) : super(key: key); + + /// The builder that will be used in case of error + final Widget Function(Error error) errorBuilder; + + /// If true a default swipe to action behaviour will be added to this widget + final bool swipeToAction; + + /// The builder used when the channel list is empty. + final WidgetBuilder emptyBuilder; + + /// The query filters to use. + /// You can query on any of the custom fields you've defined on the [Channel]. + /// You can also filter other built-in channel fields. + final Map filter; + + /// Query channels options. + /// + /// state: if true returns the Channel state + /// watch: if true listen to changes to this Channel in real time. + final Map options; + + /// The sorting used for the channels matching the filters. + /// Sorting is based on field and direction, multiple sorting options can be provided. + /// You can sort based on last_updated, last_message_at, updated_at, created_at or member_count. + /// Direction can be ascending or descending. + final List sort; + + /// Pagination parameters + /// limit: the number of users to return (max is 30) + /// offset: the offset (max is 1000) + /// message_limit: how many messages should be included to each channel + final PaginationParams pagination; + + /// Function called when tapping on a channel + /// By default it calls [Navigator.push] building a [MaterialPageRoute] + /// with the widget [userWidget] as child. + final UserTapCallback onUserTap; + + /// Function called when long pressing on a channel + final Function(User) onUserLongPress; + + /// Widget used when opening a channel + final Widget userWidget; + + // /// Builder used to create a custom channel preview + // final ChannelPreviewBuilder channelPreviewBuilder; + + /// Builder used to create a custom item separator + final Function(BuildContext, int) separatorBuilder; + + /// The function called when the image is tapped + final Function(User) onImageTap; + + /// Set it to false to disable the pull-to-refresh widget + final bool pullToRefresh; + + @override + _UserListViewState createState() => _UserListViewState(); +} + +class _UserListViewState extends State + with WidgetsBindingObserver { + final ScrollController _scrollController = ScrollController(); + + @override + void initState() { + super.initState(); + } + + @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, + ); + }, + child: _buildListView(usersBloc), + ); + } + + StreamBuilder> _buildListView( + UsersBlocState usersBlocState, + ) { + return StreamBuilder( + stream: usersBlocState.usersStream, + builder: (context, snapshot) { + if (snapshot.hasError) { + if (snapshot.error is Error) { + print((snapshot.error as Error).stackTrace); + } + + 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 (!snapshot.hasData) { + return LayoutBuilder( + builder: (context, viewportConstraints) { + return SingleChildScrollView( + physics: AlwaysScrollableScrollPhysics(), + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: viewportConstraints.maxHeight, + ), + child: Center( + child: CircularProgressIndicator(), + ), + ), + ); + }, + ); + } + + final users = snapshot.data; + + if (users.isEmpty && widget.emptyBuilder != null) { + return widget.emptyBuilder(context); + } + + if (users.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'), + ), + ), + ); + }, + ); + } + + return ListView.custom( + physics: AlwaysScrollableScrollPhysics(), + controller: _scrollController, + childrenDelegate: SliverChildBuilderDelegate( + (context, i) { + return _itemBuilder(context, i, users); + }, + childCount: (users.length * 2) + 1, + findChildIndexCallback: (key) { + final ValueKey valueKey = key; + final index = users.indexWhere( + (channel) => 'CHANNEL-${channel.id}' == valueKey.value); + return index != -1 ? (index * 2) : null; + }, + ), + ); + }, + ); + } + + Widget _itemBuilder(context, int i, List users) { + if (i % 2 != 0) { + if (widget.separatorBuilder != null) { + return widget.separatorBuilder(context, i); + } + return _separatorBuilder(context, i); + } + + i = i ~/ 2; + + final usersProvider = UsersBloc.of(context); + if (i < users.length) { + final user = users[i]; + + UserTapCallback onTap; + if (widget.onUserTap != null) { + onTap = widget.onUserTap; + } else { + onTap = (client, _) { + // Navigator.push( + // context, + // MaterialPageRoute( + // builder: (context) { + // return StreamChannel( + // child: widget.userWidget, + // channel: client, + // ); + // }, + // ), + // ); + }; + } + + return Container( + key: ValueKey('USER-${user.id}'), + child: Builder( + builder: (context) { + Widget child; + child = ListTile( + title: Text(user.name), + ); + // if (widget.channelPreviewBuilder != null) { + // child = Stack( + // children: [ + // widget.channelPreviewBuilder( + // context, + // channel, + // ), + // Positioned.fill( + // child: Material( + // color: Colors.transparent, + // child: InkWell( + // onTap: () { + // onTap(channel, widget.channelWidget); + // }, + // ), + // ), + // ), + // ], + // ); + // } else { + // final backgroundColor = + // Theme.of(context).brightness == Brightness.light + // ? Color(0xffEBEBEB) + // : Color(0xff141414); + // child = Slidable( + // enabled: widget.swipeToAction, + // actionPane: SlidableBehindActionPane(), + // actionExtentRatio: 0.12, + // closeOnScroll: true, + // secondaryActions: [ + // IconSlideAction( + // color: backgroundColor, + // icon: Icons.more_horiz, + // onTap: () { + // showModalBottomSheet( + // clipBehavior: Clip.hardEdge, + // shape: RoundedRectangleBorder( + // borderRadius: BorderRadius.only( + // topLeft: Radius.circular(32), + // topRight: Radius.circular(32), + // ), + // ), + // context: context, + // builder: (context) { + // return ChannelBottomSheet(channel: channel); + // }, + // ); + // }, + // ), + // IconSlideAction( + // color: backgroundColor, + // icon: StreamIcons.mute, + // onTap: () async { + // if (!channel.isMuted) { + // await channel.mute(); + // } else { + // await channel.unmute(); + // } + // }, + // ), + // if (channel.isGroup && !channel.isDistinct) + // IconSlideAction( + // color: backgroundColor, + // icon: StreamIcons.user_minus, + // onTap: () async { + // final confirm = await showConfirmationDialog( + // context, + // 'Do you want to leave the group?', + // ); + // if (confirm == true) { + // await channel + // .removeMembers([StreamChat.of(context).user.id]); + // } + // }, + // ), + // if (!channel.isGroup && !channel.isDistinct) + // IconSlideAction( + // color: backgroundColor, + // icon: Icons.delete_outline, + // onTap: () async { + // final confirm = await showConfirmationDialog( + // context, + // 'Do you want to delete the chat?', + // ); + // if (confirm == true) { + // await channel + // .removeMembers([StreamChat.of(context).user.id]); + // } + // }, + // ), + // ], + // child: Container( + // color: StreamChatTheme.of(context).backgroundColor, + // child: ChannelPreview( + // onLongPress: widget.onChannelLongPress, + // channel: channel, + // onImageTap: widget.onImageTap != null + // ? () { + // widget.onImageTap(channel); + // } + // : null, + // onTap: (channel) { + // onTap(channel, widget.userWidget); + // }, + // ), + // ), + // ); + // } + return child; + }, + ), + ); + } else { + return _buildQueryProgressIndicator(context, usersProvider); + } + } + + Widget _buildQueryProgressIndicator(context, UsersBlocState usersProvider) { + return StreamBuilder( + stream: usersProvider.queryUsersLoading, + initialData: false, + builder: (context, snapshot) { + if (snapshot.hasError) { + return Container( + color: Color(0xffd0021B).withAlpha(26), + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 16.0), + child: Center( + child: Text('Error loading users'), + ), + ), + ); + } + return Container( + height: 100, + padding: EdgeInsets.all(32), + child: Center( + child: snapshot.data ? CircularProgressIndicator() : Container(), + ), + ); + }); + } + + Widget _separatorBuilder(context, i) { + return Container( + height: 1, + color: Theme.of(context).brightness == Brightness.dark + ? Colors.white.withOpacity(0.1) + : Colors.black.withOpacity(0.1), + ); + } +} diff --git a/lib/src/users_bloc.dart b/lib/src/users_bloc.dart new file mode 100644 index 00000000..da2221e0 --- /dev/null +++ b/lib/src/users_bloc.dart @@ -0,0 +1,114 @@ +import 'package:flutter/material.dart'; +import 'package:rxdart/rxdart.dart'; +import 'package:stream_chat/stream_chat.dart'; + +import 'stream_chat.dart'; + +/// Widget dedicated to the management of a users list with pagination +class UsersBloc extends StatefulWidget { + /// The widget child + final Widget child; + + /// Instantiate a new UsersBloc + const UsersBloc({ + Key key, + @required this.child, + }) : super(key: key); + + @override + UsersBlocState createState() => UsersBlocState(); + + /// Use this method to get the current [UsersBlocState] instance + static UsersBlocState of(BuildContext context) { + UsersBlocState state; + + state = context.findAncestorStateOfType(); + + if (state == null) { + throw Exception('You must have a UsersBloc widget as ancestor'); + } + + return state; + } +} + +/// The current state of the [UsersBloc] +class UsersBlocState extends State + with AutomaticKeepAliveClientMixin { + /// The current users list + List get users => _usersController.value; + + /// The current users list as a stream + Stream> get usersStream => _usersController.stream; + + final BehaviorSubject> _usersController = BehaviorSubject(); + + final BehaviorSubject _queryUsersLoadingController = + BehaviorSubject.seeded(false); + + /// The stream notifying the state of queryUsers call + Stream get queryUsersLoading => _queryUsersLoadingController.stream; + + /// Calls [Client.queryUsers] updating [queryUsersLoading] stream + Future queryUsers({ + Map filter, + List sort, + Map options, + PaginationParams pagination, + }) async { + final client = StreamChat.of(context).client; + + if (client.state?.user == null || + _queryUsersLoadingController.value == true) { + return; + } + _queryUsersLoadingController.add(true); + try { + final clear = pagination == null || + pagination.offset == null || + pagination.offset == 0; + + final oldUsers = List.from(users ?? []); + + final usersResponse = await client.queryUsers( + filter: filter, + sort: sort, + options: options, + pagination: pagination, + ); + + if (clear) { + _usersController.add(usersResponse.users); + } else { + final temp = oldUsers + usersResponse.users; + _usersController.add(temp); + } + + _queryUsersLoadingController.add(false); + } catch (err, stackTrace) { + _queryUsersLoadingController.addError(err, stackTrace); + } + } + + @override + void initState() { + super.initState(); + final client = StreamChat.of(context).client; + } + + @override + Widget build(BuildContext context) { + super.build(context); + return widget.child; + } + + @override + void dispose() { + _usersController.close(); + _queryUsersLoadingController.close(); + super.dispose(); + } + + @override + bool get wantKeepAlive => true; +} diff --git a/lib/stream_chat_flutter.dart b/lib/stream_chat_flutter.dart index e433e58e..3c9554fc 100644 --- a/lib/stream_chat_flutter.dart +++ b/lib/stream_chat_flutter.dart @@ -30,3 +30,4 @@ export 'src/typing_indicator.dart'; export 'src/user_avatar.dart'; export 'src/utils.dart'; export 'src/video_attachment.dart'; +export 'src/user_list_view.dart'; From e5b25d8d06d57896dfbbe9fc08ceb470b727f78e Mon Sep 17 00:00:00 2001 From: xsahil03x Date: Tue, 17 Nov 2020 23:32:16 +0530 Subject: [PATCH 04/40] feat: user list --- example/lib/main.dart | 14 ++- lib/src/user_item.dart | 69 +++++++++++++ lib/src/user_list_view.dart | 187 ++++++++++++----------------------- lib/src/users_bloc.dart | 6 -- lib/stream_chat_flutter.dart | 1 + 5 files changed, 148 insertions(+), 129 deletions(-) create mode 100644 lib/src/user_item.dart diff --git a/example/lib/main.dart b/example/lib/main.dart index 7c288eae..5fba1c06 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -464,7 +464,19 @@ class _NewChatScreenState extends State { style: TextStyle(color: Colors.black), ), ), - body: Container(), + body: UsersBloc( + child: UserListView( + pagination: PaginationParams( + limit: 25, + ), + sort: [ + SortOption( + 'name', + direction: SortOption.ASC, + ), + ], + ), + ), ); } } diff --git a/lib/src/user_item.dart b/lib/src/user_item.dart new file mode 100644 index 00000000..14b488c7 --- /dev/null +++ b/lib/src/user_item.dart @@ -0,0 +1,69 @@ +import 'package:flutter/material.dart'; +import 'package:jiffy/jiffy.dart'; +import 'package:stream_chat/stream_chat.dart'; +import 'package:stream_chat_flutter/src/user_list_view.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +import 'stream_chat_theme.dart'; + +/// +/// It shows the current [User] preview. +/// +/// The widget uses a [StreamBuilder] to render the user information image as soon as it updates. +/// +/// Usually you don't use this widget as it's the default user preview used by [UserListView]. +/// +/// The widget renders the ui based on the first ancestor of type [StreamChatTheme]. +/// Modify it to change the widget appearance. +class UserItem extends StatelessWidget { + /// Instantiate a new UserItem + const UserItem( + {Key key, + @required this.user, + this.onTap, + this.onLongPress, + this.onImageTap}) + : super(key: key); + + /// Function called when tapping this widget + final void Function(User) onTap; + + /// Function called when long pressing this widget + final void Function(User) onLongPress; + + /// User displayed + final User user; + + /// The function called when the image is tapped + final void Function(User) onImageTap; + + @override + Widget build(BuildContext context) { + return ListTile( + onTap: () { + if (onTap != null) { + onTap(user); + } + }, + onLongPress: () { + if (onLongPress != null) { + onLongPress(user); + } + }, + leading: UserAvatar( + user: user, + showOnlineStatus: true, + onTap: (user) { + if (onImageTap != null) { + onImageTap(user); + } + }), + title: Text(user.name), + subtitle: _buildLastActive(context), + ); + } + + Widget _buildLastActive(context) { + return Text('Last seen ${Jiffy(user.lastActive).fromNow()}'); + } +} diff --git a/lib/src/user_list_view.dart b/lib/src/user_list_view.dart index 5b661ee8..93c21849 100644 --- a/lib/src/user_list_view.dart +++ b/lib/src/user_list_view.dart @@ -1,11 +1,18 @@ +import 'dart:convert'; + import 'package:flutter/material.dart'; import 'package:stream_chat/stream_chat.dart'; import 'package:stream_chat_flutter/src/users_bloc.dart'; import 'stream_chat.dart'; +import 'user_item.dart'; +/// Callback called when tapping on a user typedef UserTapCallback = void Function(User, Widget); +/// Builder used to create a custom [UserItem] from a [User] +typedef UserItemBuilder = Widget Function(BuildContext, User); + /// /// It shows the list of current users. /// @@ -50,6 +57,7 @@ class UserListView extends StatefulWidget { this.onUserTap, this.onUserLongPress, this.userWidget, + this.userItemBuilder, this.separatorBuilder, this.onImageTap, this.swipeToAction = false, @@ -99,8 +107,8 @@ class UserListView extends StatefulWidget { /// Widget used when opening a channel final Widget userWidget; - // /// Builder used to create a custom channel preview - // final ChannelPreviewBuilder channelPreviewBuilder; + /// Builder used to create a custom user preview + final UserItemBuilder userItemBuilder; /// Builder used to create a custom item separator final Function(BuildContext, int) separatorBuilder; @@ -122,6 +130,21 @@ class _UserListViewState extends State @override void initState() { super.initState(); + final channelsBloc = UsersBloc.of(context); + channelsBloc.queryUsers( + filter: widget.filter, + sort: widget.sort, + pagination: widget.pagination, + options: widget.options, + ); + + _scrollController.addListener(() { + channelsBloc.queryUsersLoading.first.then((loading) { + if (!loading) { + _listenUserPagination(channelsBloc); + } + }); + }); } @override @@ -263,8 +286,8 @@ class _UserListViewState extends State childCount: (users.length * 2) + 1, findChildIndexCallback: (key) { final ValueKey valueKey = key; - final index = users.indexWhere( - (channel) => 'CHANNEL-${channel.id}' == valueKey.value); + final index = users + .indexWhere((user) => 'USER-${user.id}' == valueKey.value); return index != -1 ? (index * 2) : null; }, ), @@ -306,125 +329,12 @@ class _UserListViewState extends State }; } - return Container( + return UserItem( key: ValueKey('USER-${user.id}'), - child: Builder( - builder: (context) { - Widget child; - child = ListTile( - title: Text(user.name), - ); - // if (widget.channelPreviewBuilder != null) { - // child = Stack( - // children: [ - // widget.channelPreviewBuilder( - // context, - // channel, - // ), - // Positioned.fill( - // child: Material( - // color: Colors.transparent, - // child: InkWell( - // onTap: () { - // onTap(channel, widget.channelWidget); - // }, - // ), - // ), - // ), - // ], - // ); - // } else { - // final backgroundColor = - // Theme.of(context).brightness == Brightness.light - // ? Color(0xffEBEBEB) - // : Color(0xff141414); - // child = Slidable( - // enabled: widget.swipeToAction, - // actionPane: SlidableBehindActionPane(), - // actionExtentRatio: 0.12, - // closeOnScroll: true, - // secondaryActions: [ - // IconSlideAction( - // color: backgroundColor, - // icon: Icons.more_horiz, - // onTap: () { - // showModalBottomSheet( - // clipBehavior: Clip.hardEdge, - // shape: RoundedRectangleBorder( - // borderRadius: BorderRadius.only( - // topLeft: Radius.circular(32), - // topRight: Radius.circular(32), - // ), - // ), - // context: context, - // builder: (context) { - // return ChannelBottomSheet(channel: channel); - // }, - // ); - // }, - // ), - // IconSlideAction( - // color: backgroundColor, - // icon: StreamIcons.mute, - // onTap: () async { - // if (!channel.isMuted) { - // await channel.mute(); - // } else { - // await channel.unmute(); - // } - // }, - // ), - // if (channel.isGroup && !channel.isDistinct) - // IconSlideAction( - // color: backgroundColor, - // icon: StreamIcons.user_minus, - // onTap: () async { - // final confirm = await showConfirmationDialog( - // context, - // 'Do you want to leave the group?', - // ); - // if (confirm == true) { - // await channel - // .removeMembers([StreamChat.of(context).user.id]); - // } - // }, - // ), - // if (!channel.isGroup && !channel.isDistinct) - // IconSlideAction( - // color: backgroundColor, - // icon: Icons.delete_outline, - // onTap: () async { - // final confirm = await showConfirmationDialog( - // context, - // 'Do you want to delete the chat?', - // ); - // if (confirm == true) { - // await channel - // .removeMembers([StreamChat.of(context).user.id]); - // } - // }, - // ), - // ], - // child: Container( - // color: StreamChatTheme.of(context).backgroundColor, - // child: ChannelPreview( - // onLongPress: widget.onChannelLongPress, - // channel: channel, - // onImageTap: widget.onImageTap != null - // ? () { - // widget.onImageTap(channel); - // } - // : null, - // onTap: (channel) { - // onTap(channel, widget.userWidget); - // }, - // ), - // ), - // ); - // } - return child; - }, - ), + user: user, + onTap: (user) => onTap(user, widget.userWidget), + onLongPress: widget.onUserLongPress, + onImageTap: widget.onImageTap, ); } else { return _buildQueryProgressIndicator(context, usersProvider); @@ -465,4 +375,37 @@ class _UserListViewState extends State : Colors.black.withOpacity(0.1), ); } + + void _listenUserPagination(UsersBlocState usersProvider) { + if (_scrollController.position.maxScrollExtent == + _scrollController.offset && + _scrollController.offset != 0) { + 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 channelsBloc = UsersBloc.of(context); + channelsBloc.queryUsers( + filter: widget.filter, + sort: widget.sort, + pagination: widget.pagination, + options: widget.options, + ); + } + } } diff --git a/lib/src/users_bloc.dart b/lib/src/users_bloc.dart index da2221e0..91cbaf80 100644 --- a/lib/src/users_bloc.dart +++ b/lib/src/users_bloc.dart @@ -90,12 +90,6 @@ class UsersBlocState extends State } } - @override - void initState() { - super.initState(); - final client = StreamChat.of(context).client; - } - @override Widget build(BuildContext context) { super.build(context); diff --git a/lib/stream_chat_flutter.dart b/lib/stream_chat_flutter.dart index 3c9554fc..663e72a9 100644 --- a/lib/stream_chat_flutter.dart +++ b/lib/stream_chat_flutter.dart @@ -30,4 +30,5 @@ export 'src/typing_indicator.dart'; export 'src/user_avatar.dart'; export 'src/utils.dart'; export 'src/video_attachment.dart'; +export 'src/users_bloc.dart'; export 'src/user_list_view.dart'; From 9b36c2348ebbd65374c4a49d561609a7045ef70b Mon Sep 17 00:00:00 2001 From: xsahil03x Date: Wed, 18 Nov 2020 00:09:07 +0530 Subject: [PATCH 05/40] feat: add selection property to user list item --- lib/src/user_item.dart | 27 ++++++++++++++++++++------- lib/src/user_list_view.dart | 6 ++++++ 2 files changed, 26 insertions(+), 7 deletions(-) diff --git a/lib/src/user_item.dart b/lib/src/user_item.dart index 14b488c7..ef442440 100644 --- a/lib/src/user_item.dart +++ b/lib/src/user_item.dart @@ -17,13 +17,14 @@ import 'stream_chat_theme.dart'; /// Modify it to change the widget appearance. class UserItem extends StatelessWidget { /// Instantiate a new UserItem - const UserItem( - {Key key, - @required this.user, - this.onTap, - this.onLongPress, - this.onImageTap}) - : super(key: key); + const UserItem({ + Key key, + @required this.user, + this.onTap, + this.onLongPress, + this.onImageTap, + this.selected = false, + }) : super(key: key); /// Function called when tapping this widget final void Function(User) onTap; @@ -37,6 +38,9 @@ class UserItem extends StatelessWidget { /// The function called when the image is tapped final void Function(User) onImageTap; + /// If true the [UserItem] will show a trailing checkmark + final bool selected; + @override Widget build(BuildContext context) { return ListTile( @@ -58,6 +62,15 @@ class UserItem extends StatelessWidget { onImageTap(user); } }), + trailing: selected + ? CircleAvatar( + child: Icon( + StreamIcons.check, + size: 20, + ), + radius: 10, + ) + : null, title: Text(user.name), subtitle: _buildLastActive(context), ); diff --git a/lib/src/user_list_view.dart b/lib/src/user_list_view.dart index 93c21849..f4487dc0 100644 --- a/lib/src/user_list_view.dart +++ b/lib/src/user_list_view.dart @@ -60,6 +60,7 @@ class UserListView extends StatefulWidget { this.userItemBuilder, this.separatorBuilder, this.onImageTap, + this.selectedUsers, this.swipeToAction = false, this.pullToRefresh = true, }) : super(key: key); @@ -119,6 +120,9 @@ class UserListView extends StatefulWidget { /// Set it to false to disable the pull-to-refresh widget final bool pullToRefresh; + /// Sets a blue trailing checkMark in [UserItem] for all the [selectedUsers] + final List selectedUsers; + @override _UserListViewState createState() => _UserListViewState(); } @@ -309,6 +313,7 @@ class _UserListViewState extends State final usersProvider = UsersBloc.of(context); if (i < users.length) { final user = users[i]; + final selected = widget.selectedUsers?.contains(user) ?? false; UserTapCallback onTap; if (widget.onUserTap != null) { @@ -335,6 +340,7 @@ class _UserListViewState extends State onTap: (user) => onTap(user, widget.userWidget), onLongPress: widget.onUserLongPress, onImageTap: widget.onImageTap, + selected: selected, ); } else { return _buildQueryProgressIndicator(context, usersProvider); From c8001de7ad0c13ac6883ac34628a115fb63e457d Mon Sep 17 00:00:00 2001 From: xsahil03x Date: Wed, 18 Nov 2020 00:09:57 +0530 Subject: [PATCH 06/40] [CreateChannelPage] Refactor to use newly build UserListView --- example/lib/main.dart | 93 +++++++++++-------------------------------- 1 file changed, 23 insertions(+), 70 deletions(-) diff --git a/example/lib/main.dart b/example/lib/main.dart index 5fba1c06..0f84c3a8 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -268,12 +268,7 @@ class CreateChannelPage extends StatefulWidget { } class _CreateChannelPageState extends State { - final ScrollController _scrollController = ScrollController(); - Client client; - List users = []; List selectedUsers = []; - int offset = 0; - bool loading = false; @override Widget build(BuildContext context) { @@ -292,31 +287,29 @@ class _CreateChannelPageState extends State { ); } - ListView _buildListView() { - return ListView.builder( - controller: _scrollController, - itemBuilder: _itemBuilder, - itemCount: users.length, - ); - } - - Widget _itemBuilder(context, i) { - final user = users[i]; - return ListTile( - onLongPress: () { - _selectUser(user); - }, - selected: selectedUsers.contains(user), - onTap: () { - if (selectedUsers.isNotEmpty) { - return _selectUser(user); - } - _createChannel(context, [user]); - }, - leading: UserAvatar( - user: user, + Widget _buildListView() { + return UsersBloc( + child: UserListView( + pagination: PaginationParams( + limit: 25, + ), + sort: [ + SortOption( + 'name', + direction: SortOption.ASC, + ), + ], + onUserLongPress: (user) { + _selectUser(user); + }, + selectedUsers: selectedUsers, + onUserTap: (user, _) { + if (selectedUsers.isNotEmpty) { + return _selectUser(user); + } + _createChannel(context, [user]); + }, ), - title: Text(user.name), ); } @@ -373,6 +366,7 @@ class _CreateChannelPageState extends State { List users, [ String name, ]) async { + final client = StreamChat.of(context).client; final channel = client.channel('messaging', extraData: { 'members': [ client.state.user.id, @@ -405,47 +399,6 @@ class _CreateChannelPageState extends State { }); } } - - @override - void initState() { - super.initState(); - - client = StreamChat.of(context).client; - - _scrollController.addListener(() async { - if (!loading && - _scrollController.offset >= - _scrollController.position.maxScrollExtent - 100) { - offset += 25; - await _queryUsers(); - } - }); - - _queryUsers(); - } - - Future _queryUsers() { - loading = true; - return client.queryUsers( - pagination: PaginationParams( - limit: 25, - offset: offset, - ), - sort: [ - SortOption( - 'name', - direction: SortOption.ASC, - ), - ], - ).then((value) { - setState(() { - users = [ - ...users, - ...value.users, - ]; - }); - }).whenComplete(() => loading = false); - } } class NewChatScreen extends StatefulWidget { From b115cd254c377981aff56737374b5b8991838493 Mon Sep 17 00:00:00 2001 From: xsahil03x Date: Wed, 18 Nov 2020 12:56:46 +0530 Subject: [PATCH 07/40] feat : Add alphabetically grouping flag to User list --- lib/src/user_list_view.dart | 166 +++++++++++++++++++++++++++--------- 1 file changed, 125 insertions(+), 41 deletions(-) diff --git a/lib/src/user_list_view.dart b/lib/src/user_list_view.dart index f4487dc0..56cb762d 100644 --- a/lib/src/user_list_view.dart +++ b/lib/src/user_list_view.dart @@ -3,6 +3,7 @@ import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:stream_chat/stream_chat.dart'; import 'package:stream_chat_flutter/src/users_bloc.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'stream_chat.dart'; import 'user_item.dart'; @@ -10,8 +11,8 @@ import 'user_item.dart'; /// Callback called when tapping on a user typedef UserTapCallback = void Function(User, Widget); -/// Builder used to create a custom [UserItem] from a [User] -typedef UserItemBuilder = Widget Function(BuildContext, User); +/// Builder used to create a custom [ListUserItem] from a [User] +typedef UserItemBuilder = Widget Function(BuildContext, User, bool); /// /// It shows the list of current users. @@ -63,6 +64,7 @@ class UserListView extends StatefulWidget { this.selectedUsers, this.swipeToAction = false, this.pullToRefresh = true, + this.groupAlphabetically = false, }) : super(key: key); /// The builder that will be used in case of error @@ -120,9 +122,14 @@ class UserListView extends StatefulWidget { /// Set it to false to disable the pull-to-refresh widget final bool pullToRefresh; - /// Sets a blue trailing checkMark in [UserItem] for all the [selectedUsers] + /// Sets a blue trailing checkMark in [ListUserItem] for all the [selectedUsers] final List selectedUsers; + /// Set it to true to group users by their first character + /// + /// defaults to false + final bool groupAlphabetically; + @override _UserListViewState createState() => _UserListViewState(); } @@ -172,11 +179,30 @@ class _UserListViewState extends State ); } - StreamBuilder> _buildListView( + StreamBuilder> _buildListView( UsersBlocState usersBlocState, ) { return StreamBuilder( - stream: usersBlocState.usersStream, + stream: usersBlocState.usersStream.map( + (users) { + if (widget.groupAlphabetically) { + final temp = users + ..sort((curr, next) => curr.name.compareTo(next.name)); + final groupedUsers = >{}; + for (var e in temp) { + final alphabet = e.name[0]; + 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(); + }, + ), builder: (context, snapshot) { if (snapshot.hasError) { if (snapshot.error is Error) { @@ -256,13 +282,13 @@ class _UserListViewState extends State ); } - final users = snapshot.data; + final items = snapshot.data; - if (users.isEmpty && widget.emptyBuilder != null) { + if (items.isEmpty && widget.emptyBuilder != null) { return widget.emptyBuilder(context); } - if (users.isEmpty && widget.emptyBuilder == null) { + if (items.isEmpty && widget.emptyBuilder == null) { return LayoutBuilder( builder: (context, viewportConstraints) { return SingleChildScrollView( @@ -285,13 +311,13 @@ class _UserListViewState extends State controller: _scrollController, childrenDelegate: SliverChildBuilderDelegate( (context, i) { - return _itemBuilder(context, i, users); + return _itemBuilder(context, i, items); }, - childCount: (users.length * 2) + 1, + childCount: (items.length * 2) + 1, findChildIndexCallback: (key) { final ValueKey valueKey = key; - final index = users - .indexWhere((user) => 'USER-${user.id}' == valueKey.value); + final index = + items.indexWhere((item) => item.key == valueKey.value); return index != -1 ? (index * 2) : null; }, ), @@ -300,7 +326,7 @@ class _UserListViewState extends State ); } - Widget _itemBuilder(context, int i, List users) { + Widget _itemBuilder(BuildContext context, int i, List items) { if (i % 2 != 0) { if (widget.separatorBuilder != null) { return widget.separatorBuilder(context, i); @@ -311,36 +337,57 @@ class _UserListViewState extends State i = i ~/ 2; final usersProvider = UsersBloc.of(context); - if (i < users.length) { - final user = users[i]; - final selected = widget.selectedUsers?.contains(user) ?? false; + if (i < items.length) { + final item = items[i]; + return item.when( + headerItem: (header) { + return Container( + key: ValueKey('HEADER-$header'), + color: Colors.grey.shade100, + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Text( + header, + style: TextStyle(fontWeight: FontWeight.w500), + ), + ), + ); + }, + userItem: (user) { + final selected = widget.selectedUsers?.contains(user) ?? false; - UserTapCallback onTap; - if (widget.onUserTap != null) { - onTap = widget.onUserTap; - } else { - onTap = (client, _) { - // Navigator.push( - // context, - // MaterialPageRoute( - // builder: (context) { - // return StreamChannel( - // child: widget.userWidget, - // channel: client, - // ); - // }, - // ), - // ); - }; - } + UserTapCallback onTap; + if (widget.onUserTap != null) { + onTap = widget.onUserTap; + } else { + onTap = (client, _) { + // Navigator.push( + // context, + // MaterialPageRoute( + // builder: (context) { + // return StreamChannel( + // child: widget.userWidget, + // channel: client, + // ); + // }, + // ), + // ); + }; + } - return UserItem( - key: ValueKey('USER-${user.id}'), - user: user, - onTap: (user) => onTap(user, widget.userWidget), - onLongPress: widget.onUserLongPress, - onImageTap: widget.onImageTap, - selected: selected, + return Container( + key: ValueKey('USER-${user.id}'), + child: widget.userItemBuilder != null + ? widget.userItemBuilder(context, user, selected) + : UserItem( + user: user, + onTap: (user) => onTap(user, widget.userWidget), + onLongPress: widget.onUserLongPress, + onImageTap: widget.onImageTap, + selected: selected, + ), + ); + }, ); } else { return _buildQueryProgressIndicator(context, usersProvider); @@ -415,3 +462,40 @@ class _UserListViewState extends State } } } + +abstract class ListItem { + String get key { + if (this is ListHeaderItem) { + final header = (this as ListHeaderItem).heading; + return 'HEADER-$header'; + } + if (this is ListUserItem) { + final user = (this as ListUserItem).user; + return 'USER-${user.id}'; + } + } + + Widget when({ + @required Widget Function(String heading) headerItem, + @required Widget Function(User user) userItem, + }) { + if (this is ListHeaderItem) { + return headerItem((this as ListHeaderItem).heading); + } + if (this is ListUserItem) { + return userItem((this as ListUserItem).user); + } + } +} + +class ListHeaderItem extends ListItem { + final String heading; + + ListHeaderItem(this.heading); +} + +class ListUserItem extends ListItem { + final User user; + + ListUserItem(this.user); +} From 4561057452f76fb8ced3af7c68d78ab6d6302b9a Mon Sep 17 00:00:00 2001 From: xsahil03x Date: Wed, 18 Nov 2020 13:30:03 +0530 Subject: [PATCH 08/40] feat : Add support for userName filtering in user list --- lib/src/user_list_view.dart | 74 ++++++++++++++++++++++++++----------- 1 file changed, 53 insertions(+), 21 deletions(-) diff --git a/lib/src/user_list_view.dart b/lib/src/user_list_view.dart index 56cb762d..57abcc99 100644 --- a/lib/src/user_list_view.dart +++ b/lib/src/user_list_view.dart @@ -1,6 +1,7 @@ import 'dart:convert'; import 'package:flutter/material.dart'; +import 'package:rxdart/rxdart.dart'; import 'package:stream_chat/stream_chat.dart'; import 'package:stream_chat_flutter/src/users_bloc.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; @@ -65,7 +66,13 @@ class UserListView extends StatefulWidget { this.swipeToAction = false, this.pullToRefresh = true, this.groupAlphabetically = false, - }) : super(key: key); + this.filterByUserName = '', + this.filterByUserNameStream, + }) : assert( + filterByUserName == null || filterByUserNameStream == null, + 'Cannot provide both filterByUserName and filterByUserNameStream.', + ), + super(key: key); /// The builder that will be used in case of error final Widget Function(Error error) errorBuilder; @@ -130,6 +137,12 @@ class UserListView extends StatefulWidget { /// defaults to false final bool groupAlphabetically; + /// + final String filterByUserName; + + /// + final Stream filterByUserNameStream; + @override _UserListViewState createState() => _UserListViewState(); } @@ -179,30 +192,49 @@ class _UserListViewState extends State ); } + List _getFilteredItems(List users, String query) { + if (widget.groupAlphabetically) { + var temp = users..sort((curr, next) => curr.name.compareTo(next.name)); + temp = temp + .where((it) => it.name.toLowerCase().contains(query.toLowerCase())); + final groupedUsers = >{}; + for (var e in temp) { + final alphabet = e.name[0]; + 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 + .where((it) => it.name.toLowerCase().contains(query.toLowerCase())) + .map((e) => ListUserItem(e)) + .toList(); + } + + Stream> _buildUserStream( + UsersBlocState usersBlocState, + ) { + if (widget.filterByUserNameStream == null) { + return usersBlocState.usersStream.map( + (users) => _getFilteredItems(users, widget.filterByUserName), + ); + } + return Rx.combineLatest2( + usersBlocState.usersStream, + widget.filterByUserNameStream, + _getFilteredItems, + ); + } + StreamBuilder> _buildListView( UsersBlocState usersBlocState, ) { return StreamBuilder( - stream: usersBlocState.usersStream.map( - (users) { - if (widget.groupAlphabetically) { - final temp = users - ..sort((curr, next) => curr.name.compareTo(next.name)); - final groupedUsers = >{}; - for (var e in temp) { - final alphabet = e.name[0]; - 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(); - }, - ), + stream: _buildUserStream(usersBlocState), builder: (context, snapshot) { if (snapshot.hasError) { if (snapshot.error is Error) { From eda42df7da5c786dbe9c1f162922d96cb2c2cb8c Mon Sep 17 00:00:00 2001 From: xsahil03x Date: Wed, 18 Nov 2020 13:38:08 +0530 Subject: [PATCH 09/40] fix : whereIterable to List --- lib/src/user_list_view.dart | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/src/user_list_view.dart b/lib/src/user_list_view.dart index 57abcc99..1c3652ff 100644 --- a/lib/src/user_list_view.dart +++ b/lib/src/user_list_view.dart @@ -196,7 +196,8 @@ class _UserListViewState extends State if (widget.groupAlphabetically) { var temp = users..sort((curr, next) => curr.name.compareTo(next.name)); temp = temp - .where((it) => it.name.toLowerCase().contains(query.toLowerCase())); + .where((it) => it.name.toLowerCase().contains(query.toLowerCase())) + .toList(); final groupedUsers = >{}; for (var e in temp) { final alphabet = e.name[0]; From 1742fe6f5fa6b39af7caf24226f3e6a4cbe1f3ea Mon Sep 17 00:00:00 2001 From: xsahil03x Date: Wed, 18 Nov 2020 17:16:18 +0530 Subject: [PATCH 10/40] feat: Replace List with Set for easier computation. --- lib/src/user_list_view.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/user_list_view.dart b/lib/src/user_list_view.dart index 1c3652ff..c4e3f2ce 100644 --- a/lib/src/user_list_view.dart +++ b/lib/src/user_list_view.dart @@ -130,7 +130,7 @@ class UserListView extends StatefulWidget { final bool pullToRefresh; /// Sets a blue trailing checkMark in [ListUserItem] for all the [selectedUsers] - final List selectedUsers; + final Set selectedUsers; /// Set it to true to group users by their first character /// From 6411afdee00728dd01042cded5fa2ab418de89ad Mon Sep 17 00:00:00 2001 From: xsahil03x Date: Wed, 18 Nov 2020 20:15:34 +0530 Subject: [PATCH 11/40] temp push --- example/lib/chips_input_text_field.dart | 142 ++++++++++++++++++++++++ example/lib/main.dart | 114 +++++++++++++++++-- 2 files changed, 245 insertions(+), 11 deletions(-) create mode 100644 example/lib/chips_input_text_field.dart diff --git a/example/lib/chips_input_text_field.dart b/example/lib/chips_input_text_field.dart new file mode 100644 index 00000000..e62773f7 --- /dev/null +++ b/example/lib/chips_input_text_field.dart @@ -0,0 +1,142 @@ +import 'package:flutter/material.dart'; +import 'package:stream_chat_flutter/src/stream_icons.dart'; + +typedef ChipBuilder = Widget Function(BuildContext context, T chip); +typedef OnChipAdded = void Function(T chip); +typedef OnChipRemoved = void Function(T chip); + +class ChipsInputTextField extends StatefulWidget { + final TextEditingController controller; + final FocusNode focusNode; + final ValueChanged onInputChanged; + final ChipBuilder chipBuilder; + final OnChipAdded onChipAdded; + final OnChipRemoved onChipRemoved; + final String hint; + + const ChipsInputTextField({ + Key key, + @required this.chipBuilder, + @required this.controller, + this.onInputChanged, + this.focusNode, + this.onChipAdded, + this.onChipRemoved, + this.hint = 'Type a name or group', + }) : super(key: key); + + @override + ChipInputTextFieldState createState() => ChipInputTextFieldState(); +} + +class ChipInputTextFieldState extends State> { + final _chips = {}; + bool _pauseItemAddition = false; + + void addItem(T item) { + if (!_pauseItemAddition) { + setState(() => _chips.add(item)); + if (widget.onChipAdded != null) widget.onChipAdded(item); + } + } + + void removeItem(T item) { + setState(() { + _chips.remove(item); + if (_chips.isEmpty) resumeItemAddition(); + }); + if (widget.focusNode != null) widget.focusNode.requestFocus(); + if (widget.onChipRemoved != null) widget.onChipRemoved(item); + } + + void pauseItemAddition() { + if (!_pauseItemAddition) { + setState(() => _pauseItemAddition = true); + } + } + + void resumeItemAddition() { + if (_pauseItemAddition) { + setState(() => _pauseItemAddition = false); + } + } + + @override + Widget build(BuildContext context) { + return Material( + elevation: 4, + color: Colors.white, + child: Container( + child: Padding( + padding: const EdgeInsets.symmetric( + vertical: 16.0, + horizontal: 16.0, + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.symmetric(vertical: 10.0), + child: Text( + 'TO:', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500, + ), + ), + ), + SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Wrap( + spacing: 4.0, + runSpacing: 4.0, + children: _chips.map((item) { + return widget.chipBuilder(context, item); + }).toList(), + ), + if (!_pauseItemAddition) ...[ + if (_chips.isNotEmpty) SizedBox(height: 4), + TextField( + controller: widget.controller, + onChanged: widget.onInputChanged, + focusNode: widget.focusNode, + style: TextStyle(fontSize: 18), + decoration: InputDecoration( + isDense: true, + border: InputBorder.none, + focusedBorder: InputBorder.none, + enabledBorder: InputBorder.none, + errorBorder: InputBorder.none, + disabledBorder: InputBorder.none, + contentPadding: const EdgeInsets.symmetric( + vertical: 8, + ), + hintText: widget.hint, + hintStyle: TextStyle(fontSize: 18)), + ), + ] + ], + ), + ), + SizedBox(width: 12), + IconButton( + icon: Icon( + _chips.isEmpty ? StreamIcons.user : StreamIcons.user_add, + ), + onPressed: () { + resumeItemAddition(); + }, + visualDensity: VisualDensity.compact, + padding: const EdgeInsets.all(0), + ), + ], + ), + ), + ), + ); + } +} diff --git a/example/lib/main.dart b/example/lib/main.dart index 0f84c3a8..fa1c14a6 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -6,6 +6,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; +import 'chips_input_text_field.dart'; import 'notifications_service.dart'; @@ -268,7 +269,7 @@ class CreateChannelPage extends StatefulWidget { } class _CreateChannelPageState extends State { - List selectedUsers = []; + final selectedUsers = {}; @override Widget build(BuildContext context) { @@ -282,7 +283,7 @@ class _CreateChannelPageState extends State { ), ), floatingActionButton: - selectedUsers.isNotEmpty ? _buildFAB(context) : SizedBox(), + selectedUsers.isNotEmpty ? _buildFAB(context) : null, body: _buildListView(), ); } @@ -325,7 +326,7 @@ class _CreateChannelPageState extends State { } } - _createChannel(context, selectedUsers, name); + _createChannel(context, selectedUsers.toList(), name); }, ); } @@ -407,10 +408,44 @@ class NewChatScreen extends StatefulWidget { } class _NewChatScreenState extends State { + final _chipInputTextFieldStateKey = + GlobalKey>(); + + TextEditingController _controller; + + ChipInputTextFieldState get _chipInputTextFieldState => + _chipInputTextFieldStateKey.currentState; + + String _userNameQuery = ''; + + final _selectedUsers = {}; + + bool _isSearchActive = false; + + @override + void initState() { + super.initState(); + _controller = TextEditingController() + ..addListener(() { + setState(() { + _userNameQuery = _controller.text; + _isSearchActive = _userNameQuery.isNotEmpty; + }); + }); + } + + @override + void dispose() { + _controller?.clear(); + _controller?.dispose(); + super.dispose(); + } + @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( + elevation: 0, backgroundColor: Colors.white, title: Text( 'New Chat', @@ -418,14 +453,71 @@ class _NewChatScreenState extends State { ), ), body: UsersBloc( - child: UserListView( - pagination: PaginationParams( - limit: 25, - ), - sort: [ - SortOption( - 'name', - direction: SortOption.ASC, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ChipsInputTextField( + key: _chipInputTextFieldStateKey, + controller: _controller, + focusNode: FocusNode(), + chipBuilder: (context, user) { + return InputChip( + key: ObjectKey(user), + label: Text( + user.name, + style: TextStyle(color: Colors.black), + ), + avatar: UserAvatar( + user: user, + ), + onDeleted: () => _chipInputTextFieldState.removeItem(user), + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + ); + }, + onChipAdded: (user) { + setState(() => _selectedUsers.add(user)); + }, + onChipRemoved: (user) { + setState(() => _selectedUsers.remove(user)); + }, + ), + Container( + width: double.maxFinite, + color: Colors.white54, + child: Padding( + padding: const EdgeInsets.symmetric( + vertical: 8, + horizontal: 8, + ), + child: Text( + _isSearchActive + ? "Matches for \"$_userNameQuery\"" + : 'On the platform', + style: TextStyle( + fontWeight: FontWeight.w500, + ), + ), + ), + ), + Expanded( + child: UserListView( + filterByUserName: _userNameQuery, + selectedUsers: _selectedUsers, + groupAlphabetically: _isSearchActive ? false : true, + onUserTap: (user, _) { + if (!_selectedUsers.contains(user)) { + _controller.clear(); + _chipInputTextFieldState + ..addItem(user) + ..pauseItemAddition(); + } + }, + pagination: PaginationParams( + limit: 25, + ), + ), + ), + MessageInput( ), ], ), From 6c4bcd2073a5f52473d26a3304aa29ac6b3e1fa4 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Wed, 18 Nov 2020 18:06:27 +0100 Subject: [PATCH 12/40] fix messageinput --- example/lib/main.dart | 149 ++++++++++++++++++++---------------- example/pubspec.yaml | 2 +- lib/src/message_input.dart | 20 ++--- lib/src/stream_channel.dart | 4 +- pubspec.yaml | 2 +- 5 files changed, 98 insertions(+), 79 deletions(-) diff --git a/example/lib/main.dart b/example/lib/main.dart index fa1c14a6..c4917e51 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -6,8 +6,8 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; -import 'chips_input_text_field.dart'; +import 'chips_input_text_field.dart'; import 'notifications_service.dart'; void main() async { @@ -422,9 +422,12 @@ class _NewChatScreenState extends State { bool _isSearchActive = false; + Channel channel; + @override void initState() { super.initState(); + channel = StreamChat.of(context).client.channel('messaging'); _controller = TextEditingController() ..addListener(() { setState(() { @@ -452,74 +455,88 @@ class _NewChatScreenState extends State { style: TextStyle(color: Colors.black), ), ), - body: UsersBloc( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - ChipsInputTextField( - key: _chipInputTextFieldStateKey, - controller: _controller, - focusNode: FocusNode(), - chipBuilder: (context, user) { - return InputChip( - key: ObjectKey(user), - label: Text( - user.name, - style: TextStyle(color: Colors.black), - ), - avatar: UserAvatar( - user: user, - ), - onDeleted: () => _chipInputTextFieldState.removeItem(user), - materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, - ); - }, - onChipAdded: (user) { - setState(() => _selectedUsers.add(user)); - }, - onChipRemoved: (user) { - setState(() => _selectedUsers.remove(user)); - }, - ), - Container( - width: double.maxFinite, - color: Colors.white54, - child: Padding( - padding: const EdgeInsets.symmetric( - vertical: 8, - horizontal: 8, - ), - child: Text( - _isSearchActive - ? "Matches for \"$_userNameQuery\"" - : 'On the platform', - style: TextStyle( - fontWeight: FontWeight.w500, - ), - ), - ), - ), - Expanded( - child: UserListView( - filterByUserName: _userNameQuery, - selectedUsers: _selectedUsers, - groupAlphabetically: _isSearchActive ? false : true, - onUserTap: (user, _) { - if (!_selectedUsers.contains(user)) { - _controller.clear(); - _chipInputTextFieldState - ..addItem(user) - ..pauseItemAddition(); - } + body: StreamChannel( + showLoading: false, + channel: channel, + child: UsersBloc( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ChipsInputTextField( + key: _chipInputTextFieldStateKey, + controller: _controller, + focusNode: FocusNode(), + chipBuilder: (context, user) { + return InputChip( + key: ObjectKey(user), + label: Text( + user.name, + style: TextStyle(color: Colors.black), + ), + avatar: UserAvatar( + user: user, + ), + onDeleted: () => _chipInputTextFieldState.removeItem(user), + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + ); }, - pagination: PaginationParams( - limit: 25, + onChipAdded: (user) { + setState(() => _selectedUsers.add(user)); + }, + onChipRemoved: (user) { + setState(() => _selectedUsers.remove(user)); + }, + ), + Container( + width: double.maxFinite, + color: Colors.white54, + child: Padding( + padding: const EdgeInsets.symmetric( + vertical: 8, + horizontal: 8, + ), + child: Text( + _isSearchActive + ? "Matches for \"$_userNameQuery\"" + : 'On the platform', + style: TextStyle( + fontWeight: FontWeight.w500, + ), + ), ), ), - ), - MessageInput( - ), - ], + Expanded( + child: UserListView( + filterByUserName: _userNameQuery, + selectedUsers: _selectedUsers, + groupAlphabetically: _isSearchActive ? false : true, + onUserTap: (user, _) { + if (!_selectedUsers.contains(user)) { + _controller.clear(); + _chipInputTextFieldState + ..addItem(user) + ..pauseItemAddition(); + } + }, + pagination: PaginationParams( + limit: 25, + ), + ), + ), + MessageInput( + preMessageSending: (message) async { + channel.extraData = { + 'members': [ + ..._selectedUsers.map((e) => e.id), + channel.client.state.user.id, + ], + }; + await channel.watch(); + return message; + }, + ), + ], + ), ), ), ); diff --git a/example/pubspec.yaml b/example/pubspec.yaml index 8fc2dadb..fca549d2 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -1,6 +1,6 @@ name: example description: A new Flutter project. -version: 1.0.59+61 +version: 1.0.60+62 environment: sdk: ">=2.2.2 <3.0.0" diff --git a/lib/src/message_input.dart b/lib/src/message_input.dart index 6024169b..6f89db84 100644 --- a/lib/src/message_input.dart +++ b/lib/src/message_input.dart @@ -364,7 +364,10 @@ class MessageInputState extends State { controller: textEditingController, focusNode: _focusNode, onChanged: (s) { - StreamChannel.of(context).channel.keyStroke(); + StreamChannel.of(context) + .channel + .keyStroke() + .catchError((e) {}); setState(() { _messageIsPresent = s.trim().isNotEmpty; @@ -479,7 +482,9 @@ class MessageInputState extends State { void _checkMentions(String s, BuildContext context) { if (textEditingController.selection.isCollapsed && - (s.isNotEmpty && s[textEditingController.selection.start - 1] == '@' || + (s.isNotEmpty && + textEditingController.selection.start > 0 && + s[textEditingController.selection.start - 1] == '@' || textEditingController.text .substring(0, textEditingController.selection.start) .split(' ') @@ -1569,14 +1574,9 @@ class MessageInputState extends State { child: Padding( padding: const EdgeInsets.all(8.0), child: Center( - child: InkWell( - onTap: () { - sendMessage(); - }, - child: Icon( - _getIdleSendIcon(), - color: Colors.grey, - ), + child: Icon( + _getIdleSendIcon(), + color: Colors.grey, )), ), ); diff --git a/lib/src/stream_channel.dart b/lib/src/stream_channel.dart index ce19b261..6fafa070 100644 --- a/lib/src/stream_channel.dart +++ b/lib/src/stream_channel.dart @@ -12,12 +12,14 @@ class StreamChannel extends StatefulWidget { Key key, @required this.child, @required this.channel, + this.showLoading = true, }) : super( key: key, ); final Widget child; final Channel channel; + final bool showLoading; /// Use this method to get the current [StreamChannelState] instance static StreamChannelState of(BuildContext context) { @@ -155,7 +157,7 @@ class StreamChannelState extends State { future: widget.channel.initialized, initialData: widget.channel.state != null, builder: (context, snapshot) { - if (!snapshot.hasData || !snapshot.data) { + if (widget.showLoading && (!snapshot.hasData || !snapshot.data)) { return Container( height: 30, child: Center( diff --git a/pubspec.yaml b/pubspec.yaml index 9c1b0a29..7e7fe36b 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -27,7 +27,7 @@ dependencies: file_picker: ^2.0.12 image_picker: ^0.6.7+2 flutter_keyboard_visibility: ^3.3.0 - stream_chat: ^0.2.13 + stream_chat: ^0.2.13+1 mime: ^0.9.6+3 visibility_detector: ^0.1.5 http_parser: ^3.1.4 From 7733cc74591904968e92968338ada29e580c0d0d Mon Sep 17 00:00:00 2001 From: xsahil03x Date: Thu, 19 Nov 2020 12:11:57 +0530 Subject: [PATCH 13/40] [NewChatScreen] Navigate to ChannelPage on successful messageSent --- example/lib/main.dart | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/example/lib/main.dart b/example/lib/main.dart index c4917e51..0be01886 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -534,6 +534,19 @@ class _NewChatScreenState extends State { await channel.watch(); return message; }, + onMessageSent: (_) { + Navigator.pushReplacement( + context, + MaterialPageRoute( + builder: (context) { + return StreamChannel( + child: ChannelPage(), + channel: channel, + ); + }, + ), + ); + }, ), ], ), From 83d68c4baf8c60ebf5c52ce85de9cb0ee1424257 Mon Sep 17 00:00:00 2001 From: xsahil03x Date: Thu, 19 Nov 2020 13:14:08 +0530 Subject: [PATCH 14/40] [NewChatScreen] Add create group button, Finishing touches --- example/lib/chips_input_text_field.dart | 2 +- example/lib/main.dart | 28 ++++++++++++- example/lib/neumorphic_button.dart | 52 +++++++++++++++++++++++++ 3 files changed, 80 insertions(+), 2 deletions(-) create mode 100644 example/lib/neumorphic_button.dart diff --git a/example/lib/chips_input_text_field.dart b/example/lib/chips_input_text_field.dart index e62773f7..ef8229c8 100644 --- a/example/lib/chips_input_text_field.dart +++ b/example/lib/chips_input_text_field.dart @@ -64,7 +64,7 @@ class ChipInputTextFieldState extends State> { @override Widget build(BuildContext context) { return Material( - elevation: 4, + elevation: 2, color: Colors.white, child: Container( child: Padding( diff --git a/example/lib/main.dart b/example/lib/main.dart index 0be01886..f681129d 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -9,6 +9,7 @@ import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'chips_input_text_field.dart'; import 'notifications_service.dart'; +import 'neumorphic_button.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); @@ -487,9 +488,34 @@ class _NewChatScreenState extends State { setState(() => _selectedUsers.remove(user)); }, ), + if (!_isSearchActive) + Container( + color: Colors.white54, + child: InkWell( + onTap: () {}, + child: Row( + children: [ + NeumorphicButton( + child: Icon( + StreamIcons.group, + color: Colors.blue.shade700, + ), + ), + SizedBox(width: 8), + Text( + 'Create a Group', + style: TextStyle( + fontWeight: FontWeight.w500, + fontSize: 18, + ), + ), + ], + ), + ), + ), Container( width: double.maxFinite, - color: Colors.white54, + color: Colors.grey.shade50, child: Padding( padding: const EdgeInsets.symmetric( vertical: 8, diff --git a/example/lib/neumorphic_button.dart b/example/lib/neumorphic_button.dart new file mode 100644 index 00000000..c738cecd --- /dev/null +++ b/example/lib/neumorphic_button.dart @@ -0,0 +1,52 @@ +import 'package:flutter/material.dart'; + +extension ColorUtils on Color { + Color mix(Color another, double amount) { + return Color.lerp(this, another, amount); + } +} + +class NeumorphicButton extends StatelessWidget { + final Widget child; + final Color backgroundColor; + final EdgeInsets margin; + final EdgeInsets padding; + + const NeumorphicButton({ + Key key, + @required this.child, + this.backgroundColor = Colors.white, + this.margin = const EdgeInsets.all(8), + this.padding = const EdgeInsets.all(14), + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Container( + child: child, + margin: margin, + padding: padding, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: backgroundColor, + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + backgroundColor.mix(Colors.white, 0.2), + backgroundColor.mix(Colors.black, 0.1), + ]), + boxShadow: [ + BoxShadow( + blurRadius: 1, + color: backgroundColor.mix(Colors.white, 0.6), + ), + BoxShadow( + blurRadius: 1, + color: backgroundColor.mix(Colors.black, 0.3), + ) + ], + ), + ); + } +} From 1a21dc27a2f080404857aef69699dbf6233fc32f Mon Sep 17 00:00:00 2001 From: xsahil03x Date: Thu, 19 Nov 2020 13:20:55 +0530 Subject: [PATCH 15/40] [Drawer,NewChatScreen] Navigate to NewGroupChatScreen on "New Group" button press. --- example/lib/chips_input_text_field.dart | 2 +- example/lib/main.dart | 33 ++++++++++++++++++++++++- 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/example/lib/chips_input_text_field.dart b/example/lib/chips_input_text_field.dart index ef8229c8..e62773f7 100644 --- a/example/lib/chips_input_text_field.dart +++ b/example/lib/chips_input_text_field.dart @@ -64,7 +64,7 @@ class ChipInputTextFieldState extends State> { @override Widget build(BuildContext context) { return Material( - elevation: 2, + elevation: 4, color: Colors.white, child: Container( child: Padding( diff --git a/example/lib/main.dart b/example/lib/main.dart index f681129d..82fee6b2 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -121,6 +121,12 @@ class ChannelListPage extends StatelessWidget { ), ), ListTile( + onTap: () { + Navigator.push( + context, + MaterialPageRoute(builder: (_) => NewGroupChatScreen()), + ); + }, leading: Icon(StreamIcons.group), title: Text( 'New group', @@ -492,7 +498,12 @@ class _NewChatScreenState extends State { Container( color: Colors.white54, child: InkWell( - onTap: () {}, + onTap: () { + Navigator.push( + context, + MaterialPageRoute(builder: (_) => NewGroupChatScreen()), + ); + }, child: Row( children: [ NeumorphicButton( @@ -581,3 +592,23 @@ class _NewChatScreenState extends State { ); } } + +class NewGroupChatScreen extends StatefulWidget { + @override + _NewGroupChatScreenState createState() => _NewGroupChatScreenState(); +} + +class _NewGroupChatScreenState extends State { + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + backgroundColor: Colors.white, + title: Text( + 'Add Group Members', + style: TextStyle(color: Colors.black), + ), + ), + ); + } +} From 33e91ff18801dbe4205fc523b2f9c8eb0bd05531 Mon Sep 17 00:00:00 2001 From: xsahil03x Date: Thu, 19 Nov 2020 15:11:40 +0530 Subject: [PATCH 16/40] [New Group Chat] Complete add group members screen --- example/lib/main.dart | 168 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 168 insertions(+) diff --git a/example/lib/main.dart b/example/lib/main.dart index 82fee6b2..907d4ec3 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -599,15 +599,183 @@ class NewGroupChatScreen extends StatefulWidget { } class _NewGroupChatScreenState extends State { + TextEditingController _controller; + + String _userNameQuery = ''; + + final _selectedUsers = {}; + + bool _isSearchActive = false; + + @override + void initState() { + super.initState(); + _controller = TextEditingController() + ..addListener(() { + setState(() { + _userNameQuery = _controller.text; + _isSearchActive = _userNameQuery.isNotEmpty; + }); + }); + } + + @override + void dispose() { + _controller?.clear(); + _controller?.dispose(); + super.dispose(); + } + @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( + elevation: 2, backgroundColor: Colors.white, title: Text( 'Add Group Members', style: TextStyle(color: Colors.black), ), + actions: [ + if (_selectedUsers.isNotEmpty) + IconButton( + icon: Icon( + StreamIcons.arrow_right, + color: Colors.blue.shade700, + ), + onPressed: () {}, + ) + ], + ), + body: UsersBloc( + child: Column( + children: [ + Container( + decoration: BoxDecoration( + border: Border.all( + color: Colors.grey.shade300, + ), + borderRadius: BorderRadius.circular(24), + ), + margin: const EdgeInsets.symmetric( + vertical: 8, + horizontal: 8, + ), + child: TextField( + onTap: () {}, + controller: _controller, + decoration: InputDecoration( + prefixIcon: Icon(StreamIcons.search), + hintText: 'Search', + contentPadding: const EdgeInsets.all(8), + border: OutlineInputBorder( + borderSide: BorderSide.none, + borderRadius: BorderRadius.circular(24), + ), + ), + ), + ), + if (_selectedUsers.isNotEmpty) + Container( + height: 120, + 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( + user: user, + showOnlineStatus: true, + borderRadius: BorderRadius.circular(40), + constraints: BoxConstraints.tightFor( + height: 80, + width: 80, + ), + ), + Positioned( + top: 0, + right: 0, + child: InkWell( + onTap: () { + if (_selectedUsers.contains(user)) { + setState(() => _selectedUsers.remove(user)); + } + }, + child: Container( + decoration: BoxDecoration( + color: Colors.white, + shape: BoxShape.circle, + border: + Border.all(color: Colors.grey.shade100), + ), + child: Padding( + padding: const EdgeInsets.all(4.0), + child: Icon( + Icons.clear_rounded, + size: 16, + ), + ), + ), + ), + ) + ], + ), + SizedBox(height: 4), + Text( + user.name.split(' ')[0], + style: TextStyle( + fontWeight: FontWeight.w500, + fontSize: 16, + ), + ), + ], + ); + }, + ), + ), + Container( + width: double.maxFinite, + color: Colors.grey.shade50, + child: Padding( + padding: const EdgeInsets.symmetric( + vertical: 8, + horizontal: 8, + ), + child: Text( + _isSearchActive + ? 'Matches for \"$_userNameQuery\"' + : 'On the platform', + style: TextStyle( + fontWeight: FontWeight.w500, + ), + ), + ), + ), + Expanded( + child: UserListView( + filterByUserName: _userNameQuery, + selectedUsers: _selectedUsers, + groupAlphabetically: _isSearchActive ? false : true, + onUserTap: (user, _) { + if (!_selectedUsers.contains(user)) { + setState(() { + _selectedUsers.add(user); + }); + } + }, + pagination: PaginationParams( + limit: 25, + ), + ), + ), + ], + ), ), ); } From d3ae3a5e4d6d58d6c669c4661938d7165d7fd82e Mon Sep 17 00:00:00 2001 From: xsahil03x Date: Thu, 19 Nov 2020 17:06:44 +0530 Subject: [PATCH 17/40] [UserItem] Add "showLastSeen" property --- lib/src/user_item.dart | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/src/user_item.dart b/lib/src/user_item.dart index ef442440..d4331cf8 100644 --- a/lib/src/user_item.dart +++ b/lib/src/user_item.dart @@ -24,6 +24,7 @@ class UserItem extends StatelessWidget { this.onLongPress, this.onImageTap, this.selected = false, + this.showLastSeen = true, }) : super(key: key); /// Function called when tapping this widget @@ -41,6 +42,9 @@ class UserItem extends StatelessWidget { /// If true the [UserItem] will show a trailing checkmark final bool selected; + /// If true the [UserItem] will show the last seen + final bool showLastSeen; + @override Widget build(BuildContext context) { return ListTile( @@ -72,7 +76,7 @@ class UserItem extends StatelessWidget { ) : null, title: Text(user.name), - subtitle: _buildLastActive(context), + subtitle: showLastSeen ? _buildLastActive(context) : null, ); } From 382e77e4a419ef893b4115e74ef1e21e6568d8f2 Mon Sep 17 00:00:00 2001 From: xsahil03x Date: Thu, 19 Nov 2020 17:07:54 +0530 Subject: [PATCH 18/40] Expose UserItem from the package. --- lib/stream_chat_flutter.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/stream_chat_flutter.dart b/lib/stream_chat_flutter.dart index 663e72a9..91ac68c8 100644 --- a/lib/stream_chat_flutter.dart +++ b/lib/stream_chat_flutter.dart @@ -32,3 +32,4 @@ export 'src/utils.dart'; export 'src/video_attachment.dart'; export 'src/users_bloc.dart'; export 'src/user_list_view.dart'; +export 'src/user_item.dart'; From 08bc56799dc5c7c4d50829ba2c03c718a9d99829 Mon Sep 17 00:00:00 2001 From: xsahil03x Date: Thu, 19 Nov 2020 17:38:36 +0530 Subject: [PATCH 19/40] [New Group Chat] Complete name of group chat screen. --- example/lib/main.dart | 174 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 173 insertions(+), 1 deletion(-) diff --git a/example/lib/main.dart b/example/lib/main.dart index 907d4ec3..9158b6b0 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -643,7 +643,16 @@ class _NewGroupChatScreenState extends State { StreamIcons.arrow_right, color: Colors.blue.shade700, ), - onPressed: () {}, + onPressed: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (_) => GroupChatDetailsScreen( + selectedUsers: _selectedUsers.toList(growable: false), + ), + ), + ); + }, ) ], ), @@ -780,3 +789,166 @@ class _NewGroupChatScreenState extends State { ); } } + +class GroupChatDetailsScreen extends StatefulWidget { + final List selectedUsers; + + const GroupChatDetailsScreen({ + Key key, + @required this.selectedUsers, + }) : super(key: key); + + @override + _GroupChatDetailsScreenState createState() => _GroupChatDetailsScreenState(); +} + +class _GroupChatDetailsScreenState extends State { + final _selectedUsers = []; + + TextEditingController _groupNameController; + + Channel _channel; + + bool _isGroupNameEmpty = true; + + @override + void initState() { + super.initState(); + _channel = StreamChat.of(context).client.channel('messaging'); + _selectedUsers.addAll(widget.selectedUsers); + _groupNameController = TextEditingController() + ..addListener(() { + final name = _groupNameController.text; + setState(() { + _isGroupNameEmpty = name.isEmpty; + }); + }); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + elevation: 2, + backgroundColor: Colors.white, + title: Text( + 'Name of Group Chat', + style: TextStyle(color: Colors.black), + ), + bottom: PreferredSize( + preferredSize: Size.fromHeight(kToolbarHeight), + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16), + child: Row( + children: [ + Text( + 'NAME', + style: TextStyle( + fontWeight: FontWeight.w500, + fontSize: 16, + ), + ), + SizedBox(width: 16), + Expanded( + child: TextField( + controller: _groupNameController, + style: TextStyle(fontSize: 18), + decoration: InputDecoration( + isDense: true, + border: InputBorder.none, + focusedBorder: InputBorder.none, + enabledBorder: InputBorder.none, + errorBorder: InputBorder.none, + disabledBorder: InputBorder.none, + contentPadding: const EdgeInsets.symmetric( + vertical: 8, + ), + hintText: 'Choose a group chat name', + hintStyle: TextStyle(fontSize: 18)), + ), + ), + ], + ), + ), + ), + actions: [ + NeumorphicButton( + padding: const EdgeInsets.all(8), + margin: const EdgeInsets.symmetric(vertical: 8), + child: IconButton( + padding: const EdgeInsets.all(0), + icon: Icon(StreamIcons.check), + color: Colors.blue.shade700, + onPressed: _isGroupNameEmpty + ? null + : () async { + final groupName = _groupNameController.text; + final client = _channel.client; + _channel.extraData = { + 'members': [ + client.state.user.id, + ..._selectedUsers.map((e) => e.id), + ], + 'name': groupName, + }; + await _channel.watch(); + Navigator.of(context) + ..pop() + ..pushReplacement( + MaterialPageRoute( + builder: (context) { + return StreamChannel( + child: ChannelPage(), + channel: _channel, + ); + }, + ), + ); + }, + ), + ), + ], + ), + body: Column( + children: [ + Container( + width: double.maxFinite, + color: Colors.grey.shade50, + child: Padding( + padding: const EdgeInsets.symmetric( + vertical: 8, + horizontal: 8, + ), + child: Text( + '5 Members', + style: TextStyle( + fontWeight: FontWeight.w500, + ), + ), + ), + ), + Expanded( + child: ListView.separated( + itemCount: _selectedUsers.length, + separatorBuilder: (_, __) => Container( + height: 1, + color: Theme.of(context).brightness == Brightness.dark + ? Colors.white.withOpacity(0.1) + : Colors.black.withOpacity(0.1), + ), + itemBuilder: (_, index) { + final user = _selectedUsers[index]; + return UserItem( + key: ObjectKey(user), + user: user, + selected: true, + showLastSeen: false, + ); + }, + ), + ), + ], + ), + ); + } +} From 97b03910f1eeeee3670e1b3894e1b1933f9d75ab Mon Sep 17 00:00:00 2001 From: xsahil03x Date: Thu, 19 Nov 2020 17:47:50 +0530 Subject: [PATCH 20/40] Minor ui fixes --- example/lib/main.dart | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/example/lib/main.dart b/example/lib/main.dart index 9158b6b0..382c938d 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -671,7 +671,6 @@ class _NewGroupChatScreenState extends State { horizontal: 8, ), child: TextField( - onTap: () {}, controller: _controller, decoration: InputDecoration( prefixIcon: Icon(StreamIcons.search), @@ -811,6 +810,8 @@ class _GroupChatDetailsScreenState extends State { bool _isGroupNameEmpty = true; + int get _totalUsers => _selectedUsers.length; + @override void initState() { super.initState(); @@ -920,7 +921,7 @@ class _GroupChatDetailsScreenState extends State { horizontal: 8, ), child: Text( - '5 Members', + '$_totalUsers ${_totalUsers > 1 ? 'Members' : 'Member'}', style: TextStyle( fontWeight: FontWeight.w500, ), From 0138f87c75f9dce2eddd6cca6664a1a483d7f422 Mon Sep 17 00:00:00 2001 From: xsahil03x Date: Thu, 19 Nov 2020 18:18:01 +0530 Subject: [PATCH 21/40] [NewChat, NewGroup] Add emptyState widget --- example/lib/main.dart | 58 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/example/lib/main.dart b/example/lib/main.dart index 382c938d..43a92d1b 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -558,6 +558,35 @@ class _NewChatScreenState extends State { pagination: PaginationParams( limit: 25, ), + 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: Icon( + StreamIcons.search, + size: 96, + color: Colors.grey, + ), + ), + Text('No user matches these keywords...'), + ], + ), + ), + ), + ); + }, + ); + }, ), ), MessageInput( @@ -780,6 +809,35 @@ class _NewGroupChatScreenState extends State { pagination: PaginationParams( limit: 25, ), + 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: Icon( + StreamIcons.search, + size: 96, + color: Colors.grey, + ), + ), + Text('No user matches these keywords...'), + ], + ), + ), + ), + ); + }, + ); + }, ), ), ], From f3c32148029f0ef5c04e976c6c598a471ca0f342 Mon Sep 17 00:00:00 2001 From: xsahil03x Date: Thu, 19 Nov 2020 18:18:55 +0530 Subject: [PATCH 22/40] [UserListView] Cleanup --- lib/src/user_list_view.dart | 22 +--------------------- 1 file changed, 1 insertion(+), 21 deletions(-) diff --git a/lib/src/user_list_view.dart b/lib/src/user_list_view.dart index c4e3f2ce..4a3ae96c 100644 --- a/lib/src/user_list_view.dart +++ b/lib/src/user_list_view.dart @@ -388,33 +388,13 @@ class _UserListViewState extends State }, userItem: (user) { final selected = widget.selectedUsers?.contains(user) ?? false; - - UserTapCallback onTap; - if (widget.onUserTap != null) { - onTap = widget.onUserTap; - } else { - onTap = (client, _) { - // Navigator.push( - // context, - // MaterialPageRoute( - // builder: (context) { - // return StreamChannel( - // child: widget.userWidget, - // channel: client, - // ); - // }, - // ), - // ); - }; - } - return Container( key: ValueKey('USER-${user.id}'), child: widget.userItemBuilder != null ? widget.userItemBuilder(context, user, selected) : UserItem( user: user, - onTap: (user) => onTap(user, widget.userWidget), + onTap: (user) => widget.onUserTap(user, widget.userWidget), onLongPress: widget.onUserLongPress, onImageTap: widget.onImageTap, selected: selected, From 1cbb10942f20f35a8ef62a235317ffd4d0f0db2b Mon Sep 17 00:00:00 2001 From: xsahil03x Date: Thu, 19 Nov 2020 18:32:49 +0530 Subject: [PATCH 23/40] Remove old create channel screen --- example/lib/main.dart | 147 ------------------------------------------ 1 file changed, 147 deletions(-) diff --git a/example/lib/main.dart b/example/lib/main.dart index 43a92d1b..f9250efe 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -167,14 +167,6 @@ class ChannelListPage extends StatelessWidget { ), ), ), - floatingActionButton: FloatingActionButton( - child: Icon(Icons.add), - onPressed: () { - Navigator.of(context).push(MaterialPageRoute(builder: (context) { - return CreateChannelPage(); - })); - }, - ), body: ChannelsBloc( child: ChannelListView( swipeToAction: true, @@ -270,145 +262,6 @@ class ThreadPage extends StatelessWidget { } } -class CreateChannelPage extends StatefulWidget { - @override - _CreateChannelPageState createState() => _CreateChannelPageState(); -} - -class _CreateChannelPageState extends State { - final selectedUsers = {}; - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - elevation: 0, - backgroundColor: Colors.transparent, - title: Text( - 'Create a channel', - style: Theme.of(context).textTheme.headline6, - ), - ), - floatingActionButton: - selectedUsers.isNotEmpty ? _buildFAB(context) : null, - body: _buildListView(), - ); - } - - Widget _buildListView() { - return UsersBloc( - child: UserListView( - pagination: PaginationParams( - limit: 25, - ), - sort: [ - SortOption( - 'name', - direction: SortOption.ASC, - ), - ], - onUserLongPress: (user) { - _selectUser(user); - }, - selectedUsers: selectedUsers, - onUserTap: (user, _) { - if (selectedUsers.isNotEmpty) { - return _selectUser(user); - } - _createChannel(context, [user]); - }, - ), - ); - } - - Widget _buildFAB(BuildContext context) { - return FloatingActionButton( - child: Icon(Icons.done), - onPressed: () async { - String name; - if (selectedUsers.length > 1) { - name = await _showEnterNameDialog(context); - if (name?.isNotEmpty != true) { - return; - } - } - - _createChannel(context, selectedUsers.toList(), name); - }, - ); - } - - Future _showEnterNameDialog(BuildContext context) { - final controller = TextEditingController(); - return showDialog( - context: context, - builder: (context) => SimpleDialog( - contentPadding: const EdgeInsets.all(16), - title: Text('Enter a name for the channel'), - children: [ - TextField( - controller: controller, - decoration: InputDecoration( - border: OutlineInputBorder(), - ), - ), - ButtonBar( - children: [ - FlatButton( - onPressed: () => Navigator.pop(context), - child: Text('Cancel'), - ), - FlatButton( - onPressed: () => Navigator.pop(context, controller.text), - child: Text('Ok'), - ), - ], - ), - ], - ), - ); - } - - Future _createChannel( - BuildContext context, - List users, [ - String name, - ]) async { - final client = StreamChat.of(context).client; - final channel = client.channel('messaging', extraData: { - 'members': [ - client.state.user.id, - ...users.map((e) => e.id), - ], - if (name != null) 'name': name, - }); - await channel.watch(); - Navigator.pushReplacement( - context, - MaterialPageRoute( - builder: (context) { - return StreamChannel( - child: ChannelPage(), - channel: channel, - ); - }, - ), - ); - } - - void _selectUser(User user) { - if (!selectedUsers.contains(user)) { - setState(() { - selectedUsers.add(user); - }); - } else { - setState(() { - selectedUsers.remove(user); - }); - } - } -} - class NewChatScreen extends StatefulWidget { @override _NewChatScreenState createState() => _NewChatScreenState(); From 822547b6442d749b4402384b8f31323db756cb79 Mon Sep 17 00:00:00 2001 From: xsahil03x Date: Thu, 19 Nov 2020 18:40:45 +0530 Subject: [PATCH 24/40] Release textEditingController listeners in dispose. --- example/lib/main.dart | 54 +++++++++++++++++++++++++++---------------- 1 file changed, 34 insertions(+), 20 deletions(-) diff --git a/example/lib/main.dart b/example/lib/main.dart index f9250efe..baddd1ee 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -284,22 +284,24 @@ class _NewChatScreenState extends State { Channel channel; + void _userNameListener() { + setState(() { + _userNameQuery = _controller.text; + _isSearchActive = _userNameQuery.isNotEmpty; + }); + } + @override void initState() { super.initState(); channel = StreamChat.of(context).client.channel('messaging'); - _controller = TextEditingController() - ..addListener(() { - setState(() { - _userNameQuery = _controller.text; - _isSearchActive = _userNameQuery.isNotEmpty; - }); - }); + _controller = TextEditingController()..addListener(_userNameListener); } @override void dispose() { _controller?.clear(); + _controller?.removeListener(_userNameListener); _controller?.dispose(); super.dispose(); } @@ -489,21 +491,23 @@ class _NewGroupChatScreenState extends State { bool _isSearchActive = false; + void _userNameListener() { + setState(() { + _userNameQuery = _controller.text; + _isSearchActive = _userNameQuery.isNotEmpty; + }); + } + @override void initState() { super.initState(); - _controller = TextEditingController() - ..addListener(() { - setState(() { - _userNameQuery = _controller.text; - _isSearchActive = _userNameQuery.isNotEmpty; - }); - }); + _controller = TextEditingController()..addListener(_userNameListener); } @override void dispose() { _controller?.clear(); + _controller?.removeListener(_userNameListener); _controller?.dispose(); super.dispose(); } @@ -723,18 +727,28 @@ class _GroupChatDetailsScreenState extends State { int get _totalUsers => _selectedUsers.length; + void _groupNameListener() { + final name = _groupNameController.text; + setState(() { + _isGroupNameEmpty = name.isEmpty; + }); + } + @override void initState() { super.initState(); _channel = StreamChat.of(context).client.channel('messaging'); _selectedUsers.addAll(widget.selectedUsers); _groupNameController = TextEditingController() - ..addListener(() { - final name = _groupNameController.text; - setState(() { - _isGroupNameEmpty = name.isEmpty; - }); - }); + ..addListener(_groupNameListener); + } + + @override + void dispose() { + _groupNameController?.clear(); + _groupNameController?.removeListener(_groupNameListener); + _groupNameController?.dispose(); + super.dispose(); } @override From 389656a4777d34923bc82ac4172781a4ee4c63c8 Mon Sep 17 00:00:00 2001 From: xsahil03x Date: Fri, 20 Nov 2020 12:06:00 +0530 Subject: [PATCH 25/40] UI nit picks --- example/lib/chips_input_text_field.dart | 44 ++++----- example/lib/main.dart | 125 ++++++++++++++++-------- lib/src/user_item.dart | 21 ++-- lib/src/user_list_view.dart | 10 +- 4 files changed, 124 insertions(+), 76 deletions(-) diff --git a/example/lib/chips_input_text_field.dart b/example/lib/chips_input_text_field.dart index e62773f7..e3b0eae7 100644 --- a/example/lib/chips_input_text_field.dart +++ b/example/lib/chips_input_text_field.dart @@ -64,24 +64,20 @@ class ChipInputTextFieldState extends State> { @override Widget build(BuildContext context) { return Material( - elevation: 4, + elevation: 1, color: Colors.white, child: Container( child: Padding( - padding: const EdgeInsets.symmetric( - vertical: 16.0, - horizontal: 16.0, - ), + padding: const EdgeInsets.fromLTRB(16, 16, 16, 16), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Padding( - padding: const EdgeInsets.symmetric(vertical: 10.0), + padding: const EdgeInsets.symmetric(vertical: 4.0), child: Text( 'TO:', style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.w500, + color: Colors.black.withOpacity(0.5), ), ), ), @@ -92,46 +88,48 @@ class ChipInputTextFieldState extends State> { mainAxisSize: MainAxisSize.min, children: [ Wrap( - spacing: 4.0, + spacing: 8.0, runSpacing: 4.0, children: _chips.map((item) { return widget.chipBuilder(context, item); }).toList(), ), if (!_pauseItemAddition) ...[ - if (_chips.isNotEmpty) SizedBox(height: 4), TextField( controller: widget.controller, onChanged: widget.onInputChanged, focusNode: widget.focusNode, - style: TextStyle(fontSize: 18), decoration: InputDecoration( - isDense: true, - border: InputBorder.none, - focusedBorder: InputBorder.none, - enabledBorder: InputBorder.none, - errorBorder: InputBorder.none, - disabledBorder: InputBorder.none, - contentPadding: const EdgeInsets.symmetric( - vertical: 8, - ), - hintText: widget.hint, - hintStyle: TextStyle(fontSize: 18)), + isDense: true, + border: InputBorder.none, + focusedBorder: InputBorder.none, + enabledBorder: InputBorder.none, + errorBorder: InputBorder.none, + disabledBorder: InputBorder.none, + contentPadding: const EdgeInsets.only(top: 4.0), + hintText: widget.hint, + ), ), ] ], ), ), - SizedBox(width: 12), IconButton( icon: Icon( _chips.isEmpty ? StreamIcons.user : StreamIcons.user_add, + color: Colors.black.withOpacity(0.5), ), onPressed: () { resumeItemAddition(); }, + alignment: Alignment.topRight, visualDensity: VisualDensity.compact, padding: const EdgeInsets.all(0), + splashRadius: 24, + constraints: BoxConstraints.tightFor( + height: 24, + width: 24, + ), ), ], ), diff --git a/example/lib/main.dart b/example/lib/main.dart index baddd1ee..f187ac05 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -312,10 +312,12 @@ class _NewChatScreenState extends State { appBar: AppBar( elevation: 0, backgroundColor: Colors.white, + leading: const StreamBackButton(), title: Text( 'New Chat', style: TextStyle(color: Colors.black), ), + centerTitle: true, ), body: StreamChannel( showLoading: false, @@ -329,17 +331,31 @@ class _NewChatScreenState extends State { controller: _controller, focusNode: FocusNode(), chipBuilder: (context, user) { - return InputChip( - key: ObjectKey(user), - label: Text( - user.name, - style: TextStyle(color: Colors.black), - ), - avatar: UserAvatar( - user: user, - ), - onDeleted: () => _chipInputTextFieldState.removeItem(user), - materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + return Stack( + alignment: AlignmentDirectional.centerStart, + children: [ + Container( + decoration: BoxDecoration( + color: Colors.black.withOpacity(0.05), + borderRadius: BorderRadius.circular(12), + ), + padding: const EdgeInsets.only(left: 24), + child: Padding( + padding: const EdgeInsets.fromLTRB(8, 4, 12, 4), + child: Text( + user.name, + style: TextStyle(color: Colors.black), + ), + ), + ), + UserAvatar( + user: user, + constraints: BoxConstraints.tightFor( + height: 24, + width: 24, + ), + ), + ], ); }, onChipAdded: (user) { @@ -381,7 +397,17 @@ class _NewChatScreenState extends State { ), Container( width: double.maxFinite, - color: Colors.grey.shade50, + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.centerLeft, + end: Alignment.centerRight, + colors: [ + Colors.black.withOpacity(0.02), + Colors.white.withOpacity(0.05), + ], + stops: [0, 1], + ), + ), child: Padding( padding: const EdgeInsets.symmetric( vertical: 8, @@ -392,7 +418,7 @@ class _NewChatScreenState extends State { ? "Matches for \"$_userNameQuery\"" : 'On the platform', style: TextStyle( - fontWeight: FontWeight.w500, + color: Colors.black.withOpacity(0.5), ), ), ), @@ -516,12 +542,14 @@ class _NewGroupChatScreenState extends State { Widget build(BuildContext context) { return Scaffold( appBar: AppBar( - elevation: 2, + elevation: 1, backgroundColor: Colors.white, + leading: const StreamBackButton(), title: Text( 'Add Group Members', style: TextStyle(color: Colors.black), ), + centerTitle: true, actions: [ if (_selectedUsers.isNotEmpty) IconButton( @@ -546,6 +574,7 @@ class _NewGroupChatScreenState extends State { child: Column( children: [ Container( + height: 36, decoration: BoxDecoration( border: Border.all( color: Colors.grey.shade300, @@ -559,9 +588,13 @@ class _NewGroupChatScreenState extends State { child: TextField( controller: _controller, decoration: InputDecoration( - prefixIcon: Icon(StreamIcons.search), + prefixIcon: Icon( + StreamIcons.search, + color: Colors.black, + ), hintText: 'Search', - contentPadding: const EdgeInsets.all(8), + contentPadding: const EdgeInsets.all(0), + // isDense: true, border: OutlineInputBorder( borderSide: BorderSide.none, borderRadius: BorderRadius.circular(24), @@ -571,7 +604,7 @@ class _NewGroupChatScreenState extends State { ), if (_selectedUsers.isNotEmpty) Container( - height: 120, + height: 104, child: ListView.separated( scrollDirection: Axis.horizontal, itemCount: _selectedUsers.length, @@ -586,10 +619,10 @@ class _NewGroupChatScreenState extends State { UserAvatar( user: user, showOnlineStatus: true, - borderRadius: BorderRadius.circular(40), + borderRadius: BorderRadius.circular(32), constraints: BoxConstraints.tightFor( - height: 80, - width: 80, + height: 64, + width: 64, ), ), Positioned( @@ -609,10 +642,10 @@ class _NewGroupChatScreenState extends State { Border.all(color: Colors.grey.shade100), ), child: Padding( - padding: const EdgeInsets.all(4.0), + padding: const EdgeInsets.all(2.0), child: Icon( Icons.clear_rounded, - size: 16, + size: 14, ), ), ), @@ -624,8 +657,8 @@ class _NewGroupChatScreenState extends State { Text( user.name.split(' ')[0], style: TextStyle( - fontWeight: FontWeight.w500, - fontSize: 16, + fontWeight: FontWeight.bold, + fontSize: 12, ), ), ], @@ -635,7 +668,17 @@ class _NewGroupChatScreenState extends State { ), Container( width: double.maxFinite, - color: Colors.grey.shade50, + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.centerLeft, + end: Alignment.centerRight, + colors: [ + Colors.black.withOpacity(0.02), + Colors.white.withOpacity(0.05), + ], + stops: [0, 1], + ), + ), child: Padding( padding: const EdgeInsets.symmetric( vertical: 8, @@ -646,7 +689,7 @@ class _NewGroupChatScreenState extends State { ? 'Matches for \"$_userNameQuery\"' : 'On the platform', style: TextStyle( - fontWeight: FontWeight.w500, + color: Colors.black.withOpacity(0.5), ), ), ), @@ -755,42 +798,40 @@ class _GroupChatDetailsScreenState extends State { Widget build(BuildContext context) { return Scaffold( appBar: AppBar( - elevation: 2, + elevation: 1, backgroundColor: Colors.white, + leading: const StreamBackButton(), title: Text( 'Name of Group Chat', style: TextStyle(color: Colors.black), ), + centerTitle: true, bottom: PreferredSize( preferredSize: Size.fromHeight(kToolbarHeight), child: Padding( - padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16), + padding: const EdgeInsets.symmetric(vertical: 18, horizontal: 16), child: Row( children: [ Text( 'NAME', style: TextStyle( - fontWeight: FontWeight.w500, - fontSize: 16, + color: Colors.black.withOpacity(0.5), ), ), SizedBox(width: 16), Expanded( child: TextField( controller: _groupNameController, - style: TextStyle(fontSize: 18), decoration: InputDecoration( - isDense: true, - border: InputBorder.none, - focusedBorder: InputBorder.none, - enabledBorder: InputBorder.none, - errorBorder: InputBorder.none, - disabledBorder: InputBorder.none, - contentPadding: const EdgeInsets.symmetric( - vertical: 8, - ), - hintText: 'Choose a group chat name', - hintStyle: TextStyle(fontSize: 18)), + isDense: true, + border: InputBorder.none, + focusedBorder: InputBorder.none, + enabledBorder: InputBorder.none, + errorBorder: InputBorder.none, + disabledBorder: InputBorder.none, + contentPadding: const EdgeInsets.all(0), + hintText: 'Choose a group chat name', + ), ), ), ], diff --git a/lib/src/user_item.dart b/lib/src/user_item.dart index d4331cf8..0c320b7c 100644 --- a/lib/src/user_item.dart +++ b/lib/src/user_item.dart @@ -59,13 +59,18 @@ class UserItem extends StatelessWidget { } }, leading: UserAvatar( - user: user, - showOnlineStatus: true, - onTap: (user) { - if (onImageTap != null) { - onImageTap(user); - } - }), + user: user, + showOnlineStatus: true, + onTap: (user) { + if (onImageTap != null) { + onImageTap(user); + } + }, + constraints: BoxConstraints.tightFor( + height: 40, + width: 40, + ), + ), trailing: selected ? CircleAvatar( child: Icon( @@ -75,7 +80,7 @@ class UserItem extends StatelessWidget { radius: 10, ) : null, - title: Text(user.name), + title: Text(user.name,style: TextStyle(fontWeight: FontWeight.bold),), subtitle: showLastSeen ? _buildLastActive(context) : null, ); } diff --git a/lib/src/user_list_view.dart b/lib/src/user_list_view.dart index 4a3ae96c..b35c1c68 100644 --- a/lib/src/user_list_view.dart +++ b/lib/src/user_list_view.dart @@ -376,12 +376,16 @@ class _UserListViewState extends State headerItem: (header) { return Container( key: ValueKey('HEADER-$header'), - color: Colors.grey.shade100, + color: Colors.black.withOpacity(0.05), child: Padding( - padding: const EdgeInsets.all(8.0), + padding: const EdgeInsets.symmetric(horizontal:8.0,vertical: 6), child: Text( header, - style: TextStyle(fontWeight: FontWeight.w500), + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 12, + color:Colors.black.withOpacity(0.3) + ), ), ), ); From c384588d5b1533e41412a27e0831d326a4c5e06e Mon Sep 17 00:00:00 2001 From: xsahil03x Date: Fri, 20 Nov 2020 17:55:55 +0530 Subject: [PATCH 26/40] UI nit picks --- example/lib/chips_input_text_field.dart | 9 +-- example/lib/main.dart | 78 +++++++++++++++------- example/lib/neumorphic_button.dart | 38 ++++------- lib/src/user_list_view.dart | 88 +++++++++---------------- 4 files changed, 102 insertions(+), 111 deletions(-) diff --git a/example/lib/chips_input_text_field.dart b/example/lib/chips_input_text_field.dart index e3b0eae7..695783d9 100644 --- a/example/lib/chips_input_text_field.dart +++ b/example/lib/chips_input_text_field.dart @@ -34,10 +34,8 @@ class ChipInputTextFieldState extends State> { bool _pauseItemAddition = false; void addItem(T item) { - if (!_pauseItemAddition) { - setState(() => _chips.add(item)); - if (widget.onChipAdded != null) widget.onChipAdded(item); - } + setState(() => _chips.add(item)); + if (widget.onChipAdded != null) widget.onChipAdded(item); } void removeItem(T item) { @@ -108,6 +106,9 @@ class ChipInputTextFieldState extends State> { disabledBorder: InputBorder.none, contentPadding: const EdgeInsets.only(top: 4.0), hintText: widget.hint, + hintStyle: TextStyle( + color: Colors.black.withOpacity(0.5), + ), ), ), ] diff --git a/example/lib/main.dart b/example/lib/main.dart index f187ac05..9cce3cf3 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -309,6 +309,7 @@ class _NewChatScreenState extends State { @override Widget build(BuildContext context) { return Scaffold( + backgroundColor: Color.fromRGBO(252, 252, 252, 1), appBar: AppBar( elevation: 0, backgroundColor: Colors.white, @@ -367,7 +368,6 @@ class _NewChatScreenState extends State { ), if (!_isSearchActive) Container( - color: Colors.white54, child: InkWell( onTap: () { Navigator.push( @@ -375,23 +375,26 @@ class _NewChatScreenState extends State { MaterialPageRoute(builder: (_) => NewGroupChatScreen()), ); }, - child: Row( - children: [ - NeumorphicButton( - child: Icon( - StreamIcons.group, - color: Colors.blue.shade700, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 8), + child: Row( + children: [ + NeumorphicButton( + child: Icon( + StreamIcons.group, + color: Color(0xFF006CFF), + ), ), - ), - SizedBox(width: 8), - Text( - 'Create a Group', - style: TextStyle( - fontWeight: FontWeight.w500, - fontSize: 18, + SizedBox(width: 8), + Text( + 'Create a Group', + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 16, + ), ), - ), - ], + ], + ), ), ), ), @@ -425,20 +428,33 @@ class _NewChatScreenState extends State { ), Expanded( child: UserListView( - filterByUserName: _userNameQuery, selectedUsers: _selectedUsers, groupAlphabetically: _isSearchActive ? false : true, onUserTap: (user, _) { + _controller.clear(); if (!_selectedUsers.contains(user)) { - _controller.clear(); _chipInputTextFieldState ..addItem(user) ..pauseItemAddition(); + } else { + _chipInputTextFieldState.removeItem(user); } }, pagination: PaginationParams( limit: 25, ), + filter: { + if (_userNameQuery.isNotEmpty) + 'name': { + r'$autocomplete': _userNameQuery, + } + }, + sort: [ + SortOption( + 'name', + direction: 1, + ), + ], emptyBuilder: (_) { return LayoutBuilder( builder: (context, viewportConstraints) { @@ -541,6 +557,7 @@ class _NewGroupChatScreenState extends State { @override Widget build(BuildContext context) { return Scaffold( + backgroundColor: Color.fromRGBO(252, 252, 252, 1), appBar: AppBar( elevation: 1, backgroundColor: Colors.white, @@ -555,7 +572,7 @@ class _NewGroupChatScreenState extends State { IconButton( icon: Icon( StreamIcons.arrow_right, - color: Colors.blue.shade700, + color: Color(0xFF006CFF), ), onPressed: () { Navigator.push( @@ -576,6 +593,7 @@ class _NewGroupChatScreenState extends State { Container( height: 36, decoration: BoxDecoration( + color: Colors.white, border: Border.all( color: Colors.grey.shade300, ), @@ -593,8 +611,10 @@ class _NewGroupChatScreenState extends State { color: Colors.black, ), hintText: 'Search', + hintStyle: TextStyle( + color: Colors.black.withOpacity(0.5), + ), contentPadding: const EdgeInsets.all(0), - // isDense: true, border: OutlineInputBorder( borderSide: BorderSide.none, borderRadius: BorderRadius.circular(24), @@ -696,7 +716,6 @@ class _NewGroupChatScreenState extends State { ), Expanded( child: UserListView( - filterByUserName: _userNameQuery, selectedUsers: _selectedUsers, groupAlphabetically: _isSearchActive ? false : true, onUserTap: (user, _) { @@ -709,6 +728,18 @@ class _NewGroupChatScreenState extends State { pagination: PaginationParams( limit: 25, ), + filter: { + if (_userNameQuery.isNotEmpty) + 'name': { + r'$autocomplete': _userNameQuery, + } + }, + sort: [ + SortOption( + 'name', + direction: 1, + ), + ], emptyBuilder: (_) { return LayoutBuilder( builder: (context, viewportConstraints) { @@ -797,6 +828,7 @@ class _GroupChatDetailsScreenState extends State { @override Widget build(BuildContext context) { return Scaffold( + backgroundColor: Color.fromRGBO(252, 252, 252, 1), appBar: AppBar( elevation: 1, backgroundColor: Colors.white, @@ -840,12 +872,10 @@ class _GroupChatDetailsScreenState extends State { ), actions: [ NeumorphicButton( - padding: const EdgeInsets.all(8), - margin: const EdgeInsets.symmetric(vertical: 8), child: IconButton( padding: const EdgeInsets.all(0), icon: Icon(StreamIcons.check), - color: Colors.blue.shade700, + color: Color(0xFF006CFF), onPressed: _isGroupNameEmpty ? null : () async { diff --git a/example/lib/neumorphic_button.dart b/example/lib/neumorphic_button.dart index c738cecd..69454fb2 100644 --- a/example/lib/neumorphic_button.dart +++ b/example/lib/neumorphic_button.dart @@ -1,50 +1,38 @@ import 'package:flutter/material.dart'; -extension ColorUtils on Color { - Color mix(Color another, double amount) { - return Color.lerp(this, another, amount); - } -} - class NeumorphicButton extends StatelessWidget { final Widget child; final Color backgroundColor; - final EdgeInsets margin; - final EdgeInsets padding; const NeumorphicButton({ Key key, @required this.child, this.backgroundColor = Colors.white, - this.margin = const EdgeInsets.all(8), - this.padding = const EdgeInsets.all(14), }) : super(key: key); @override Widget build(BuildContext context) { return Container( child: child, - margin: margin, - padding: padding, + margin: EdgeInsets.all(8.0), + height: 40, + width: 40, decoration: BoxDecoration( - shape: BoxShape.circle, color: backgroundColor, - gradient: LinearGradient( - begin: Alignment.topLeft, - end: Alignment.bottomRight, - colors: [ - backgroundColor.mix(Colors.white, 0.2), - backgroundColor.mix(Colors.black, 0.1), - ]), + shape: BoxShape.circle, boxShadow: [ BoxShadow( - blurRadius: 1, - color: backgroundColor.mix(Colors.white, 0.6), + color: Colors.grey[700], + offset: Offset(0, 1.0), + blurRadius: 0.5, + spreadRadius: 0, ), BoxShadow( - blurRadius: 1, - color: backgroundColor.mix(Colors.black, 0.3), - ) + color: Colors.white, + offset: Offset.zero, + blurRadius: 0.5, + spreadRadius: 0, + ), ], ), ); diff --git a/lib/src/user_list_view.dart b/lib/src/user_list_view.dart index b35c1c68..b21e8c7d 100644 --- a/lib/src/user_list_view.dart +++ b/lib/src/user_list_view.dart @@ -1,7 +1,6 @@ import 'dart:convert'; import 'package:flutter/material.dart'; -import 'package:rxdart/rxdart.dart'; import 'package:stream_chat/stream_chat.dart'; import 'package:stream_chat_flutter/src/users_bloc.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; @@ -66,13 +65,7 @@ class UserListView extends StatefulWidget { this.swipeToAction = false, this.pullToRefresh = true, this.groupAlphabetically = false, - this.filterByUserName = '', - this.filterByUserNameStream, - }) : assert( - filterByUserName == null || filterByUserNameStream == null, - 'Cannot provide both filterByUserName and filterByUserNameStream.', - ), - super(key: key); + }) : super(key: key); /// The builder that will be used in case of error final Widget Function(Error error) errorBuilder; @@ -137,12 +130,6 @@ class UserListView extends StatefulWidget { /// defaults to false final bool groupAlphabetically; - /// - final String filterByUserName; - - /// - final Stream filterByUserNameStream; - @override _UserListViewState createState() => _UserListViewState(); } @@ -154,8 +141,8 @@ class _UserListViewState extends State @override void initState() { super.initState(); - final channelsBloc = UsersBloc.of(context); - channelsBloc.queryUsers( + final usersBloc = UsersBloc.of(context); + usersBloc.queryUsers( filter: widget.filter, sort: widget.sort, pagination: widget.pagination, @@ -163,9 +150,9 @@ class _UserListViewState extends State ); _scrollController.addListener(() { - channelsBloc.queryUsersLoading.first.then((loading) { + usersBloc.queryUsersLoading.first.then((loading) { if (!loading) { - _listenUserPagination(channelsBloc); + _listenUserPagination(usersBloc); } }); }); @@ -192,42 +179,28 @@ class _UserListViewState extends State ); } - List _getFilteredItems(List users, String query) { - if (widget.groupAlphabetically) { - var temp = users..sort((curr, next) => curr.name.compareTo(next.name)); - temp = temp - .where((it) => it.name.toLowerCase().contains(query.toLowerCase())) - .toList(); - final groupedUsers = >{}; - for (var e in temp) { - final alphabet = e.name[0]; - 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 - .where((it) => it.name.toLowerCase().contains(query.toLowerCase())) - .map((e) => ListUserItem(e)) - .toList(); - } - Stream> _buildUserStream( UsersBlocState usersBlocState, ) { - if (widget.filterByUserNameStream == null) { - return usersBlocState.usersStream.map( - (users) => _getFilteredItems(users, widget.filterByUserName), - ); - } - return Rx.combineLatest2( - usersBlocState.usersStream, - widget.filterByUserNameStream, - _getFilteredItems, + return usersBlocState.usersStream.map( + (users) { + if (widget.groupAlphabetically) { + var temp = users + ..sort((curr, next) => curr.name.compareTo(next.name)); + final groupedUsers = >{}; + for (var e in temp) { + final alphabet = e.name[0]; + 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(); + }, ); } @@ -378,14 +351,13 @@ class _UserListViewState extends State key: ValueKey('HEADER-$header'), color: Colors.black.withOpacity(0.05), child: Padding( - padding: const EdgeInsets.symmetric(horizontal:8.0,vertical: 6), + padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 6), child: Text( header, style: TextStyle( - fontWeight: FontWeight.bold, - fontSize: 12, - color:Colors.black.withOpacity(0.3) - ), + fontWeight: FontWeight.bold, + fontSize: 12, + color: Colors.black.withOpacity(0.3)), ), ), ); @@ -469,8 +441,8 @@ class _UserListViewState extends State widget.pagination?.toJson()?.toString() != oldWidget.pagination?.toJson()?.toString() || widget.options?.toString() != oldWidget.options?.toString()) { - final channelsBloc = UsersBloc.of(context); - channelsBloc.queryUsers( + final usersBloc = UsersBloc.of(context); + usersBloc.queryUsers( filter: widget.filter, sort: widget.sort, pagination: widget.pagination, From 409d95d86a6d3310e9bfe0b5ba60e004894b1523 Mon Sep 17 00:00:00 2001 From: xsahil03x Date: Fri, 20 Nov 2020 18:06:47 +0530 Subject: [PATCH 27/40] [UserListView] Add a "isListAlreadySorted" check before sorting list. --- lib/src/user_list_view.dart | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/lib/src/user_list_view.dart b/lib/src/user_list_view.dart index b21e8c7d..0f7b969e 100644 --- a/lib/src/user_list_view.dart +++ b/lib/src/user_list_view.dart @@ -179,14 +179,19 @@ class _UserListViewState extends State ); } + 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 - ..sort((curr, next) => curr.name.compareTo(next.name)); + 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]; From 2908cd66c5019cebcf49b54ffb4c4266dbe746b0 Mon Sep 17 00:00:00 2001 From: xsahil03x Date: Fri, 20 Nov 2020 18:11:57 +0530 Subject: [PATCH 28/40] Change last seen text to last online --- example/lib/main.dart | 2 +- lib/src/user_item.dart | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/example/lib/main.dart b/example/lib/main.dart index 9cce3cf3..ea665cb6 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -939,7 +939,7 @@ class _GroupChatDetailsScreenState extends State { key: ObjectKey(user), user: user, selected: true, - showLastSeen: false, + showLastOnline: false, ); }, ), diff --git a/lib/src/user_item.dart b/lib/src/user_item.dart index 0c320b7c..be8037b6 100644 --- a/lib/src/user_item.dart +++ b/lib/src/user_item.dart @@ -24,7 +24,7 @@ class UserItem extends StatelessWidget { this.onLongPress, this.onImageTap, this.selected = false, - this.showLastSeen = true, + this.showLastOnline = true, }) : super(key: key); /// Function called when tapping this widget @@ -43,7 +43,7 @@ class UserItem extends StatelessWidget { final bool selected; /// If true the [UserItem] will show the last seen - final bool showLastSeen; + final bool showLastOnline; @override Widget build(BuildContext context) { @@ -81,11 +81,11 @@ class UserItem extends StatelessWidget { ) : null, title: Text(user.name,style: TextStyle(fontWeight: FontWeight.bold),), - subtitle: showLastSeen ? _buildLastActive(context) : null, + subtitle: showLastOnline ? _buildLastActive(context) : null, ); } Widget _buildLastActive(context) { - return Text('Last seen ${Jiffy(user.lastActive).fromNow()}'); + return Text('Last online ${Jiffy(user.lastActive).fromNow()}'); } } From aa62d9a01de261261f85265aa1503eaaf364b486 Mon Sep 17 00:00:00 2001 From: xsahil03x Date: Fri, 20 Nov 2020 18:15:59 +0530 Subject: [PATCH 29/40] add debounce in searchQuery --- example/lib/main.dart | 25 +++++++++++++++++++------ 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/example/lib/main.dart b/example/lib/main.dart index ea665cb6..6f86ce23 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -10,6 +10,7 @@ import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'chips_input_text_field.dart'; import 'notifications_service.dart'; import 'neumorphic_button.dart'; +import 'dart:async'; void main() async { WidgetsFlutterBinding.ensureInitialized(); @@ -284,10 +285,16 @@ class _NewChatScreenState extends State { Channel channel; + Timer _debounce; + void _userNameListener() { - setState(() { - _userNameQuery = _controller.text; - _isSearchActive = _userNameQuery.isNotEmpty; + if (_debounce?.isActive ?? false) _debounce.cancel(); + _debounce = Timer(const Duration(milliseconds: 500), () { + if (mounted) + setState(() { + _userNameQuery = _controller.text; + _isSearchActive = _userNameQuery.isNotEmpty; + }); }); } @@ -533,10 +540,16 @@ class _NewGroupChatScreenState extends State { bool _isSearchActive = false; + Timer _debounce; + void _userNameListener() { - setState(() { - _userNameQuery = _controller.text; - _isSearchActive = _userNameQuery.isNotEmpty; + if (_debounce?.isActive ?? false) _debounce.cancel(); + _debounce = Timer(const Duration(milliseconds: 500), () { + if (mounted) + setState(() { + _userNameQuery = _controller.text; + _isSearchActive = _userNameQuery.isNotEmpty; + }); }); } From 4ded4d577d56e5c9711add180c8d12977e254bdf Mon Sep 17 00:00:00 2001 From: xsahil03x Date: Fri, 20 Nov 2020 18:38:07 +0530 Subject: [PATCH 30/40] Hide Create group button once a user is selected --- example/lib/main.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/example/lib/main.dart b/example/lib/main.dart index 6f86ce23..b0f5d73f 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -373,7 +373,7 @@ class _NewChatScreenState extends State { setState(() => _selectedUsers.remove(user)); }, ), - if (!_isSearchActive) + if (!_isSearchActive && !_selectedUsers.isNotEmpty) Container( child: InkWell( onTap: () { From daf8758f61aebf701ce6924d586f25307464e7d5 Mon Sep 17 00:00:00 2001 From: xsahil03x Date: Fri, 20 Nov 2020 18:58:53 +0530 Subject: [PATCH 31/40] Fix UserAdd IconButton alignment --- example/lib/chips_input_text_field.dart | 123 ++++++++++++------------ 1 file changed, 63 insertions(+), 60 deletions(-) diff --git a/example/lib/chips_input_text_field.dart b/example/lib/chips_input_text_field.dart index 695783d9..1eb3159d 100644 --- a/example/lib/chips_input_text_field.dart +++ b/example/lib/chips_input_text_field.dart @@ -67,72 +67,75 @@ class ChipInputTextFieldState extends State> { child: Container( child: Padding( padding: const EdgeInsets.fromLTRB(16, 16, 16, 16), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: const EdgeInsets.symmetric(vertical: 4.0), - child: Text( - 'TO:', - style: TextStyle( - color: Colors.black.withOpacity(0.5), + child: IntrinsicHeight( + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.symmetric(vertical: 4.0), + child: Text( + 'TO:', + style: TextStyle( + color: Colors.black.withOpacity(0.5), + ), ), ), - ), - SizedBox(width: 12), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - Wrap( - spacing: 8.0, - runSpacing: 4.0, - children: _chips.map((item) { - return widget.chipBuilder(context, item); - }).toList(), - ), - if (!_pauseItemAddition) ...[ - TextField( - controller: widget.controller, - onChanged: widget.onInputChanged, - focusNode: widget.focusNode, - decoration: InputDecoration( - isDense: true, - border: InputBorder.none, - focusedBorder: InputBorder.none, - enabledBorder: InputBorder.none, - errorBorder: InputBorder.none, - disabledBorder: InputBorder.none, - contentPadding: const EdgeInsets.only(top: 4.0), - hintText: widget.hint, - hintStyle: TextStyle( - color: Colors.black.withOpacity(0.5), + SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Wrap( + spacing: 8.0, + runSpacing: 4.0, + children: _chips.map((item) { + return widget.chipBuilder(context, item); + }).toList(), + ), + if (!_pauseItemAddition) + TextField( + controller: widget.controller, + onChanged: widget.onInputChanged, + focusNode: widget.focusNode, + decoration: InputDecoration( + isDense: true, + border: InputBorder.none, + focusedBorder: InputBorder.none, + enabledBorder: InputBorder.none, + errorBorder: InputBorder.none, + disabledBorder: InputBorder.none, + contentPadding: const EdgeInsets.only(top: 4.0), + hintText: widget.hint, + hintStyle: TextStyle( + color: Colors.black.withOpacity(0.5), + ), ), ), - ), - ] - ], + ], + ), ), - ), - IconButton( - icon: Icon( - _chips.isEmpty ? StreamIcons.user : StreamIcons.user_add, - color: Colors.black.withOpacity(0.5), + SizedBox(width: 12), + Align( + alignment: Alignment.bottomCenter, + child: IconButton( + icon: Icon( + _chips.isEmpty ? StreamIcons.user : StreamIcons.user_add, + color: Colors.black.withOpacity(0.5), + ), + onPressed: !_pauseItemAddition ? null : resumeItemAddition, + alignment: Alignment.topRight, + visualDensity: VisualDensity.compact, + padding: const EdgeInsets.all(0), + splashRadius: 24, + constraints: BoxConstraints.tightFor( + height: 24, + width: 24, + ), + ), ), - onPressed: () { - resumeItemAddition(); - }, - alignment: Alignment.topRight, - visualDensity: VisualDensity.compact, - padding: const EdgeInsets.all(0), - splashRadius: 24, - constraints: BoxConstraints.tightFor( - height: 24, - width: 24, - ), - ), - ], + ], + ), ), ), ), From 8bc9e62917d85a66ef7a22703ed615c754603ae7 Mon Sep 17 00:00:00 2001 From: xsahil03x Date: Fri, 20 Nov 2020 18:59:36 +0530 Subject: [PATCH 32/40] Reduce debounce duration to 350ms from 500ms --- example/lib/main.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/example/lib/main.dart b/example/lib/main.dart index b0f5d73f..2a30af4f 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -289,7 +289,7 @@ class _NewChatScreenState extends State { void _userNameListener() { if (_debounce?.isActive ?? false) _debounce.cancel(); - _debounce = Timer(const Duration(milliseconds: 500), () { + _debounce = Timer(const Duration(milliseconds: 350), () { if (mounted) setState(() { _userNameQuery = _controller.text; @@ -544,7 +544,7 @@ class _NewGroupChatScreenState extends State { void _userNameListener() { if (_debounce?.isActive ?? false) _debounce.cancel(); - _debounce = Timer(const Duration(milliseconds: 500), () { + _debounce = Timer(const Duration(milliseconds: 350), () { if (mounted) setState(() { _userNameQuery = _controller.text; From cc22b1b4ae8c9b7ffd5600fa3054480d62452e6b Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Fri, 20 Nov 2020 14:55:38 +0100 Subject: [PATCH 33/40] fix ui issues --- example/lib/chips_input_text_field.dart | 5 ++- example/lib/main.dart | 43 +++++++++++++++++-------- example/pubspec.yaml | 2 +- lib/src/message_input.dart | 6 ++-- lib/src/user_avatar.dart | 15 +++++---- lib/src/user_item.dart | 5 ++- 6 files changed, 50 insertions(+), 26 deletions(-) diff --git a/example/lib/chips_input_text_field.dart b/example/lib/chips_input_text_field.dart index 1eb3159d..30183926 100644 --- a/example/lib/chips_input_text_field.dart +++ b/example/lib/chips_input_text_field.dart @@ -69,13 +69,14 @@ class ChipInputTextFieldState extends State> { padding: const EdgeInsets.fromLTRB(16, 16, 16, 16), child: IntrinsicHeight( child: Row( - crossAxisAlignment: CrossAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.baseline, children: [ Padding( padding: const EdgeInsets.symmetric(vertical: 4.0), child: Text( 'TO:', style: TextStyle( + fontSize: 12, color: Colors.black.withOpacity(0.5), ), ), @@ -109,6 +110,7 @@ class ChipInputTextFieldState extends State> { hintText: widget.hint, hintStyle: TextStyle( color: Colors.black.withOpacity(0.5), + fontSize: 14, ), ), ), @@ -122,6 +124,7 @@ class ChipInputTextFieldState extends State> { icon: Icon( _chips.isEmpty ? StreamIcons.user : StreamIcons.user_add, color: Colors.black.withOpacity(0.5), + size: 24, ), onPressed: !_pauseItemAddition ? null : resumeItemAddition, alignment: Alignment.topRight, diff --git a/example/lib/main.dart b/example/lib/main.dart index 2a30af4f..758e7784 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'dart:io'; import 'package:example/choose_user_page.dart'; @@ -8,9 +9,8 @@ import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'chips_input_text_field.dart'; -import 'notifications_service.dart'; import 'neumorphic_button.dart'; -import 'dart:async'; +import 'notifications_service.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); @@ -179,7 +179,6 @@ class ChannelListPage extends StatelessWidget { options: { 'presence': true, }, - sort: [SortOption('last_message_at')], pagination: PaginationParams( limit: 20, ), @@ -323,7 +322,10 @@ class _NewChatScreenState extends State { leading: const StreamBackButton(), title: Text( 'New Chat', - style: TextStyle(color: Colors.black), + style: TextStyle( + color: Colors.black, + fontSize: 16, + ), ), centerTitle: true, ), @@ -577,7 +579,10 @@ class _NewGroupChatScreenState extends State { leading: const StreamBackButton(), title: Text( 'Add Group Members', - style: TextStyle(color: Colors.black), + style: TextStyle( + color: Colors.black, + fontSize: 16, + ), ), centerTitle: true, actions: [ @@ -622,10 +627,12 @@ class _NewGroupChatScreenState extends State { prefixIcon: Icon( StreamIcons.search, color: Colors.black, + size: 24, ), hintText: 'Search', hintStyle: TextStyle( color: Colors.black.withOpacity(0.5), + fontSize: 14, ), contentPadding: const EdgeInsets.all(0), border: OutlineInputBorder( @@ -650,6 +657,7 @@ class _NewGroupChatScreenState extends State { Stack( children: [ UserAvatar( + onlineIndicatorAlignment: Alignment(0.9, 0.9), user: user, showOnlineStatus: true, borderRadius: BorderRadius.circular(32), @@ -659,9 +667,9 @@ class _NewGroupChatScreenState extends State { ), ), Positioned( - top: 0, - right: 0, - child: InkWell( + top: -4, + right: -4, + child: GestureDetector( onTap: () { if (_selectedUsers.contains(user)) { setState(() => _selectedUsers.remove(user)); @@ -671,14 +679,15 @@ class _NewGroupChatScreenState extends State { decoration: BoxDecoration( color: Colors.white, shape: BoxShape.circle, - border: - Border.all(color: Colors.grey.shade100), + border: Border.all( + color: Colors.grey.shade100, + ), ), child: Padding( - padding: const EdgeInsets.all(2.0), + padding: const EdgeInsets.all(0.0), child: Icon( - Icons.clear_rounded, - size: 14, + StreamIcons.close, + size: 24, ), ), ), @@ -848,7 +857,10 @@ class _GroupChatDetailsScreenState extends State { leading: const StreamBackButton(), title: Text( 'Name of Group Chat', - style: TextStyle(color: Colors.black), + style: TextStyle( + color: Colors.black, + fontSize: 16, + ), ), centerTitle: true, bottom: PreferredSize( @@ -860,6 +872,7 @@ class _GroupChatDetailsScreenState extends State { Text( 'NAME', style: TextStyle( + fontSize: 12, color: Colors.black.withOpacity(0.5), ), ), @@ -876,6 +889,8 @@ class _GroupChatDetailsScreenState extends State { disabledBorder: InputBorder.none, contentPadding: const EdgeInsets.all(0), hintText: 'Choose a group chat name', + hintStyle: TextStyle( + fontSize: 14, color: Colors.black.withOpacity(.5)), ), ), ), diff --git a/example/pubspec.yaml b/example/pubspec.yaml index fca549d2..0fe6a0ad 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -1,6 +1,6 @@ name: example description: A new Flutter project. -version: 1.0.60+62 +version: 1.0.62+64 environment: sdk: ">=2.2.2 <3.0.0" diff --git a/lib/src/message_input.dart b/lib/src/message_input.dart index cd96119a..423d4c8f 100644 --- a/lib/src/message_input.dart +++ b/lib/src/message_input.dart @@ -423,7 +423,7 @@ class MessageInputState extends State { } void _onChanged(BuildContext context, String s) { - StreamChannel.of(context).channel.keyStroke(); + StreamChannel.of(context).channel.keyStroke().catchError((e) {}); setState(() { _messageIsPresent = s.trim().isNotEmpty; @@ -1712,7 +1712,9 @@ class MessageInputState extends State { if (!kIsWeb) { _keyboardListener = KeyboardVisibility.onChange.listen((visible) { - _onChanged(context, textEditingController.text); + if (_focusNode.hasFocus) { + _onChanged(context, textEditingController.text); + } }); } diff --git a/lib/src/user_avatar.dart b/lib/src/user_avatar.dart index 66edba39..566ef603 100644 --- a/lib/src/user_avatar.dart +++ b/lib/src/user_avatar.dart @@ -13,9 +13,11 @@ class UserAvatar extends StatelessWidget { this.onTap, this.showOnlineStatus = true, this.borderRadius, + this.onlineIndicatorAlignment = Alignment.topRight, }) : super(key: key); final User user; + final Alignment onlineIndicatorAlignment; final BoxConstraints constraints; final BorderRadius borderRadius; final BoxConstraints onlineIndicatorConstraints; @@ -60,11 +62,11 @@ class UserAvatar extends StatelessWidget { ), ), if (showOnlineStatus && user.online == true) - Positioned( - top: 0, - right: 0, - child: Material( - child: Center( + Positioned.fill( + child: Align( + alignment: onlineIndicatorAlignment, + child: Material( + type: MaterialType.circle, child: Container( padding: const EdgeInsets.all(2.0), constraints: onlineIndicatorConstraints ?? @@ -77,9 +79,8 @@ class UserAvatar extends StatelessWidget { color: Color(0xff20E070), ), ), + color: Colors.white, ), - shape: CircleBorder(), - color: Colors.white, ), ), ], diff --git a/lib/src/user_item.dart b/lib/src/user_item.dart index be8037b6..06ab0848 100644 --- a/lib/src/user_item.dart +++ b/lib/src/user_item.dart @@ -80,7 +80,10 @@ class UserItem extends StatelessWidget { radius: 10, ) : null, - title: Text(user.name,style: TextStyle(fontWeight: FontWeight.bold),), + title: Text( + user.name, + style: TextStyle(fontWeight: FontWeight.bold), + ), subtitle: showLastOnline ? _buildLastActive(context) : null, ); } From 879f5d31bc3c0e7e5ee5c9bcc6a99da85729090e Mon Sep 17 00:00:00 2001 From: xsahil03x Date: Fri, 20 Nov 2020 19:31:13 +0530 Subject: [PATCH 34/40] UI fixes --- example/lib/main.dart | 50 ++++++++++++++++++++++++++++++++++++++----- 1 file changed, 45 insertions(+), 5 deletions(-) diff --git a/example/lib/main.dart b/example/lib/main.dart index 2a30af4f..cc64cbd7 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -736,6 +736,10 @@ class _NewGroupChatScreenState extends State { setState(() { _selectedUsers.add(user); }); + } else { + setState(() { + _selectedUsers.remove(user); + }); } }, pagination: PaginationParams( @@ -939,7 +943,7 @@ class _GroupChatDetailsScreenState extends State { ), Expanded( child: ListView.separated( - itemCount: _selectedUsers.length, + itemCount: _selectedUsers.length + 1, separatorBuilder: (_, __) => Container( height: 1, color: Theme.of(context).brightness == Brightness.dark @@ -947,12 +951,48 @@ class _GroupChatDetailsScreenState extends State { : Colors.black.withOpacity(0.1), ), itemBuilder: (_, index) { + if (index == _selectedUsers.length) { + return Container( + height: 1, + color: Theme.of(context).brightness == Brightness.dark + ? Colors.white.withOpacity(0.1) + : Colors.black.withOpacity(0.1), + ); + } final user = _selectedUsers[index]; - return UserItem( + return ListTile( key: ObjectKey(user), - user: user, - selected: true, - showLastOnline: false, + 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: Colors.black, + ), + padding: const EdgeInsets.all(0), + splashRadius: 24, + onPressed: () { + setState(() { + _selectedUsers.remove(user); + }); + if (_selectedUsers.isEmpty) { + Navigator.pop(context); + } + }, + ), ); }, ), From 682c0b3e10736746b8d7f73cc38b93e03fb87946 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Fri, 20 Nov 2020 18:03:33 +0100 Subject: [PATCH 35/40] swap user list with message list and create channel with draft: true --- example/lib/chips_input_text_field.dart | 157 +++++----- example/lib/main.dart | 382 ++++++++++++++---------- example/pubspec.yaml | 2 +- lib/src/message_input.dart | 8 +- lib/src/message_list_view.dart | 19 ++ lib/src/url_attachment.dart | 6 +- 6 files changed, 339 insertions(+), 235 deletions(-) diff --git a/example/lib/chips_input_text_field.dart b/example/lib/chips_input_text_field.dart index 30183926..317a68f7 100644 --- a/example/lib/chips_input_text_field.dart +++ b/example/lib/chips_input_text_field.dart @@ -22,7 +22,7 @@ class ChipsInputTextField extends StatefulWidget { this.focusNode, this.onChipAdded, this.onChipRemoved, - this.hint = 'Type a name or group', + this.hint = 'Type a name', }) : super(key: key); @override @@ -61,83 +61,96 @@ class ChipInputTextFieldState extends State> { @override Widget build(BuildContext context) { - return Material( - elevation: 1, - color: Colors.white, - child: Container( - child: Padding( - padding: const EdgeInsets.fromLTRB(16, 16, 16, 16), - child: IntrinsicHeight( - child: Row( - crossAxisAlignment: CrossAxisAlignment.baseline, - children: [ - Padding( - padding: const EdgeInsets.symmetric(vertical: 4.0), - child: Text( - 'TO:', - style: TextStyle( - fontSize: 12, - color: Colors.black.withOpacity(0.5), + return GestureDetector( + onTap: _pauseItemAddition + ? () { + setState(() { + _pauseItemAddition = false; + widget.focusNode?.requestFocus(); + }); + } + : null, + child: Material( + elevation: 1, + color: Colors.white, + child: Container( + child: Padding( + padding: const EdgeInsets.fromLTRB(16, 16, 16, 16), + child: IntrinsicHeight( + child: Row( + crossAxisAlignment: CrossAxisAlignment.baseline, + children: [ + Padding( + padding: const EdgeInsets.symmetric(vertical: 4.0), + child: Text( + 'TO:', + style: TextStyle( + fontSize: 12, + color: Colors.black.withOpacity(0.5), + ), ), ), - ), - SizedBox(width: 12), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - Wrap( - spacing: 8.0, - runSpacing: 4.0, - children: _chips.map((item) { - return widget.chipBuilder(context, item); - }).toList(), - ), - if (!_pauseItemAddition) - TextField( - controller: widget.controller, - onChanged: widget.onInputChanged, - focusNode: widget.focusNode, - decoration: InputDecoration( - isDense: true, - border: InputBorder.none, - focusedBorder: InputBorder.none, - enabledBorder: InputBorder.none, - errorBorder: InputBorder.none, - disabledBorder: InputBorder.none, - contentPadding: const EdgeInsets.only(top: 4.0), - hintText: widget.hint, - hintStyle: TextStyle( - color: Colors.black.withOpacity(0.5), - fontSize: 14, + SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Wrap( + spacing: 8.0, + runSpacing: 4.0, + children: _chips.map((item) { + return widget.chipBuilder(context, item); + }).toList(), + ), + if (!_pauseItemAddition) + TextField( + controller: widget.controller, + onChanged: widget.onInputChanged, + focusNode: widget.focusNode, + decoration: InputDecoration( + isDense: true, + border: InputBorder.none, + focusedBorder: InputBorder.none, + enabledBorder: InputBorder.none, + errorBorder: InputBorder.none, + disabledBorder: InputBorder.none, + contentPadding: const EdgeInsets.only(top: 4.0), + hintText: widget.hint, + hintStyle: TextStyle( + color: Colors.black.withOpacity(0.5), + fontSize: 14, + ), ), ), - ), - ], - ), - ), - SizedBox(width: 12), - Align( - alignment: Alignment.bottomCenter, - child: IconButton( - icon: Icon( - _chips.isEmpty ? StreamIcons.user : StreamIcons.user_add, - color: Colors.black.withOpacity(0.5), - size: 24, - ), - onPressed: !_pauseItemAddition ? null : resumeItemAddition, - alignment: Alignment.topRight, - visualDensity: VisualDensity.compact, - padding: const EdgeInsets.all(0), - splashRadius: 24, - constraints: BoxConstraints.tightFor( - height: 24, - width: 24, + ], ), ), - ), - ], + SizedBox(width: 12), + Align( + alignment: Alignment.bottomCenter, + child: IconButton( + icon: Icon( + _chips.isEmpty + ? StreamIcons.user + : StreamIcons.user_add, + color: Colors.black.withOpacity(0.5), + size: 24, + ), + onPressed: + !_pauseItemAddition ? null : resumeItemAddition, + alignment: Alignment.topRight, + visualDensity: VisualDensity.compact, + padding: const EdgeInsets.all(0), + splashRadius: 24, + constraints: BoxConstraints.tightFor( + height: 24, + width: 24, + ), + ), + ), + ], + ), ), ), ), diff --git a/example/lib/main.dart b/example/lib/main.dart index 70c11940..e2a8a5f7 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -111,6 +111,7 @@ class ChannelListPage extends StatelessWidget { ), ListTile( onTap: () { + Navigator.pop(context); Navigator.push( context, MaterialPageRoute(builder: (_) => NewChatScreen()), @@ -126,6 +127,7 @@ class ChannelListPage extends StatelessWidget { ), ListTile( onTap: () { + Navigator.pop(context); Navigator.push( context, MaterialPageRoute(builder: (_) => NewGroupChatScreen()), @@ -177,7 +179,10 @@ class ChannelListPage extends StatelessWidget { filter: { 'members': { '\$in': [user.id], - } + }, + 'draft': { + r'$ne': true, + }, }, options: { 'presence': true, @@ -283,12 +288,17 @@ class _NewChatScreenState extends State { final _selectedUsers = {}; + final _searchFocusNode = FocusNode(); + final _messageInputFocusNode = FocusNode(); + bool _isSearchActive = false; Channel channel; Timer _debounce; + bool _showUserList = true; + void _userNameListener() { if (_debounce?.isActive ?? false) _debounce.cancel(); _debounce = Timer(const Duration(milliseconds: 350), () { @@ -305,10 +315,50 @@ class _NewChatScreenState extends State { super.initState(); channel = StreamChat.of(context).client.channel('messaging'); _controller = TextEditingController()..addListener(_userNameListener); + + _searchFocusNode.addListener(() async { + if (_searchFocusNode.hasFocus && !_showUserList) { + if (channel.extraData['draft'] == true) { + await channel.stopWatching(); + channel.dispose(); + channel.client.state.channels.remove(channel.cid); + } + setState(() { + _showUserList = true; + }); + } + }); + + _messageInputFocusNode.addListener(() async { + if (_messageInputFocusNode.hasFocus && _selectedUsers.isNotEmpty) { + final chatState = StreamChat.of(context); + + channel = chatState.client.channel( + 'messaging', + extraData: { + 'members': [ + ..._selectedUsers.map((e) => e.id), + chatState.user.id, + ], + 'draft': true, + }, + ); + + if (!chatState.client.state.channels.containsKey(channel.cid)) { + await channel.watch(); + } + + setState(() { + _showUserList = false; + }); + } + }); } @override void dispose() { + _searchFocusNode.dispose(); + _messageInputFocusNode.dispose(); _controller?.clear(); _controller?.removeListener(_userNameListener); _controller?.dispose(); @@ -335,81 +385,81 @@ class _NewChatScreenState extends State { body: StreamChannel( showLoading: false, channel: channel, - child: UsersBloc( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - ChipsInputTextField( - key: _chipInputTextFieldStateKey, - controller: _controller, - focusNode: FocusNode(), - chipBuilder: (context, user) { - return Stack( - alignment: AlignmentDirectional.centerStart, - children: [ - Container( - decoration: BoxDecoration( - color: Colors.black.withOpacity(0.05), - borderRadius: BorderRadius.circular(12), - ), - padding: const EdgeInsets.only(left: 24), - child: Padding( - padding: const EdgeInsets.fromLTRB(8, 4, 12, 4), - child: Text( - user.name, - style: TextStyle(color: Colors.black), - ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ChipsInputTextField( + key: _chipInputTextFieldStateKey, + controller: _controller, + focusNode: _searchFocusNode, + chipBuilder: (context, user) { + return Stack( + alignment: AlignmentDirectional.centerStart, + children: [ + Container( + decoration: BoxDecoration( + color: Colors.black.withOpacity(0.05), + borderRadius: BorderRadius.circular(12), + ), + padding: const EdgeInsets.only(left: 24), + child: Padding( + padding: const EdgeInsets.fromLTRB(8, 4, 12, 4), + child: Text( + user.name, + style: TextStyle(color: Colors.black), ), ), - UserAvatar( - user: user, - constraints: BoxConstraints.tightFor( - height: 24, - width: 24, + ), + UserAvatar( + user: user, + constraints: BoxConstraints.tightFor( + height: 24, + width: 24, + ), + ), + ], + ); + }, + onChipAdded: (user) { + setState(() => _selectedUsers.add(user)); + }, + onChipRemoved: (user) { + setState(() => _selectedUsers.remove(user)); + }, + ), + if (!_isSearchActive && !_selectedUsers.isNotEmpty) + Container( + child: InkWell( + onTap: () { + Navigator.push( + context, + MaterialPageRoute(builder: (_) => NewGroupChatScreen()), + ); + }, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 8), + child: Row( + children: [ + NeumorphicButton( + child: Icon( + StreamIcons.group, + color: Color(0xFF006CFF), + ), ), - ), - ], - ); - }, - onChipAdded: (user) { - setState(() => _selectedUsers.add(user)); - }, - onChipRemoved: (user) { - setState(() => _selectedUsers.remove(user)); - }, - ), - if (!_isSearchActive && !_selectedUsers.isNotEmpty) - Container( - child: InkWell( - onTap: () { - Navigator.push( - context, - MaterialPageRoute(builder: (_) => NewGroupChatScreen()), - ); - }, - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 8), - child: Row( - children: [ - NeumorphicButton( - child: Icon( - StreamIcons.group, - color: Color(0xFF006CFF), - ), + SizedBox(width: 8), + Text( + 'Create a Group', + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 16, ), - SizedBox(width: 8), - Text( - 'Create a Group', - style: TextStyle( - fontWeight: FontWeight.bold, - fontSize: 16, - ), - ), - ], - ), + ), + ], ), ), ), + ), + if (_showUserList) Container( width: double.maxFinite, decoration: BoxDecoration( @@ -438,97 +488,108 @@ class _NewChatScreenState extends State { ), ), ), - Expanded( - 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, - } - }, - 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: Icon( - StreamIcons.search, - size: 96, - color: Colors.grey, + Expanded( + child: _showUserList + ? 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: Icon( + StreamIcons.search, + size: 96, + color: Colors.grey, + ), + ), + Text( + 'No user matches these keywords...'), + ], ), ), - Text('No user matches these keywords...'), - ], - ), - ), - ), - ); - }, - ); - }, - ), - ), - MessageInput( - preMessageSending: (message) async { - channel.extraData = { - 'members': [ - ..._selectedUsers.map((e) => e.id), - channel.client.state.user.id, - ], - }; - await channel.watch(); - return message; - }, - onMessageSent: (_) { - Navigator.pushReplacement( - context, - MaterialPageRoute( - builder: (context) { - return StreamChannel( - child: ChannelPage(), - channel: channel, - ); - }, - ), - ); - }, - ), - ], - ), + ), + ); + }, + ); + }, + ), + ) + : MessageListView(), + ), + MessageInput( + focusNode: _messageInputFocusNode, + onMessageSent: (m) { + if (!m.isEphemeral) { + _updateChannelAndNavigate(context); + } else { + channel.on('message.new').first.then((_) { + _updateChannelAndNavigate(context); + }); + } + }, + ), + ], ), ), ); } + + void _updateChannelAndNavigate(BuildContext context) { + channel.update({ + 'draft': false, + }); + Navigator.pushReplacement( + context, + MaterialPageRoute( + builder: (context) { + return StreamChannel( + child: ChannelPage(), + channel: channel, + ); + }, + ), + ); + } } class NewGroupChatScreen extends StatefulWidget { @@ -761,7 +822,10 @@ class _NewGroupChatScreenState extends State { if (_userNameQuery.isNotEmpty) 'name': { r'$autocomplete': _userNameQuery, - } + }, + 'id': { + r'$ne': StreamChat.of(context).user.id, + } }, sort: [ SortOption( @@ -832,9 +896,11 @@ class _GroupChatDetailsScreenState extends State { void _groupNameListener() { final name = _groupNameController.text; - setState(() { - _isGroupNameEmpty = name.isEmpty; - }); + if (mounted) { + setState(() { + _isGroupNameEmpty = name.isEmpty; + }); + } } @override diff --git a/example/pubspec.yaml b/example/pubspec.yaml index e5c11897..0fe6a0ad 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -1,6 +1,6 @@ name: example description: A new Flutter project. -version: 1.0.61+63 +version: 1.0.62+64 environment: sdk: ">=2.2.2 <3.0.0" diff --git a/lib/src/message_input.dart b/lib/src/message_input.dart index 423d4c8f..6845d1a4 100644 --- a/lib/src/message_input.dart +++ b/lib/src/message_input.dart @@ -104,6 +104,7 @@ class MessageInput extends StatefulWidget { this.actions, this.actionsLocation = ActionsLocation.left, this.attachmentThumbnailBuilders, + this.focusNode, }) : super(key: key); /// Message to edit @@ -149,6 +150,9 @@ class MessageInput extends StatefulWidget { /// Map that defines a thumbnail builder for an attachment type final Map attachmentThumbnailBuilders; + /// The focus node associated to the TextField + final FocusNode focusNode; + @override MessageInputState createState() => MessageInputState(); @@ -169,10 +173,10 @@ class MessageInput extends StatefulWidget { class MessageInputState extends State { final List<_SendingAttachment> _attachments = []; - final _focusNode = FocusNode(); final List _mentionedUsers = []; final _imagePicker = ImagePicker(); + FocusNode _focusNode; bool _inputEnabled = true; bool _messageIsPresent = false; bool _animateContainer = true; @@ -1708,6 +1712,8 @@ class MessageInputState extends State { void initState() { super.initState(); + _focusNode = widget.focusNode ?? FocusNode(); + _emojiNames = Emoji.all().map((e) => e.name); if (!kIsWeb) { diff --git a/lib/src/message_list_view.dart b/lib/src/message_list_view.dart index 2a60b75b..4a5e445b 100644 --- a/lib/src/message_list_view.dart +++ b/lib/src/message_list_view.dart @@ -180,7 +180,26 @@ class _MessageListViewState extends State { ? streamChannel.channel.state.threads[widget.parentMessage.id] : streamChannel.channel.state.messages, builder: (context, snapshot) { + if (!snapshot.hasData) { + return Center( + child: CircularProgressIndicator(), + ); + } + final messages = snapshot.data?.reversed?.toList() ?? []; + + if (messages.isEmpty) { + return Center( + child: Text( + 'No chats here yet...', + style: TextStyle( + fontSize: 12, + color: Colors.black.withOpacity(.5), + ), + ), + ); + } + return Stack( alignment: Alignment.center, children: [ diff --git a/lib/src/url_attachment.dart b/lib/src/url_attachment.dart index a873217f..d735ad67 100644 --- a/lib/src/url_attachment.dart +++ b/lib/src/url_attachment.dart @@ -4,9 +4,9 @@ import 'package:stream_chat_flutter/src/utils.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; class UrlAttachment extends StatelessWidget { - Attachment urlAttachment; - String hostDisplayName; - EdgeInsets textPadding; + final Attachment urlAttachment; + final String hostDisplayName; + final EdgeInsets textPadding; UrlAttachment({ @required this.urlAttachment, From d7a8d4f3f9111fbacf40e26bda15cefc072cb20a Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Sat, 21 Nov 2020 11:24:38 +0100 Subject: [PATCH 36/40] extract screens --- example/lib/group_chat_details_screen.dart | 221 ++++++ example/lib/main.dart | 822 +-------------------- example/lib/new_chat_screen.dart | 331 +++++++++ example/lib/new_group_chat_screen.dart | 285 +++++++ example/pubspec.yaml | 2 +- 5 files changed, 840 insertions(+), 821 deletions(-) create mode 100644 example/lib/group_chat_details_screen.dart create mode 100644 example/lib/new_chat_screen.dart create mode 100644 example/lib/new_group_chat_screen.dart diff --git a/example/lib/group_chat_details_screen.dart b/example/lib/group_chat_details_screen.dart new file mode 100644 index 00000000..8ba8c71f --- /dev/null +++ b/example/lib/group_chat_details_screen.dart @@ -0,0 +1,221 @@ +import 'package:flutter/material.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +import 'main.dart'; +import 'neumorphic_button.dart'; + +class GroupChatDetailsScreen extends StatefulWidget { + final List selectedUsers; + + const GroupChatDetailsScreen({ + Key key, + @required this.selectedUsers, + }) : super(key: key); + + @override + _GroupChatDetailsScreenState createState() => _GroupChatDetailsScreenState(); +} + +class _GroupChatDetailsScreenState extends State { + final _selectedUsers = []; + + TextEditingController _groupNameController; + + Channel _channel; + + bool _isGroupNameEmpty = true; + + int get _totalUsers => _selectedUsers.length; + + void _groupNameListener() { + final name = _groupNameController.text; + if (mounted) { + setState(() { + _isGroupNameEmpty = name.isEmpty; + }); + } + } + + @override + void initState() { + super.initState(); + _channel = StreamChat.of(context).client.channel('messaging'); + _selectedUsers.addAll(widget.selectedUsers); + _groupNameController = TextEditingController() + ..addListener(_groupNameListener); + } + + @override + void dispose() { + _groupNameController?.clear(); + _groupNameController?.removeListener(_groupNameListener); + _groupNameController?.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Color.fromRGBO(252, 252, 252, 1), + appBar: AppBar( + elevation: 1, + backgroundColor: Colors.white, + leading: const StreamBackButton(), + title: Text( + 'Name of Group Chat', + style: TextStyle( + color: Colors.black, + fontSize: 16, + ), + ), + centerTitle: true, + bottom: PreferredSize( + preferredSize: Size.fromHeight(kToolbarHeight), + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 18, horizontal: 16), + child: Row( + children: [ + Text( + 'NAME', + style: TextStyle( + fontSize: 12, + color: Colors.black.withOpacity(0.5), + ), + ), + SizedBox(width: 16), + Expanded( + child: TextField( + controller: _groupNameController, + decoration: InputDecoration( + isDense: true, + border: InputBorder.none, + focusedBorder: InputBorder.none, + enabledBorder: InputBorder.none, + errorBorder: InputBorder.none, + disabledBorder: InputBorder.none, + contentPadding: const EdgeInsets.all(0), + hintText: 'Choose a group chat name', + hintStyle: TextStyle( + fontSize: 14, color: Colors.black.withOpacity(.5)), + ), + ), + ), + ], + ), + ), + ), + actions: [ + NeumorphicButton( + child: IconButton( + padding: const EdgeInsets.all(0), + icon: Icon(StreamIcons.check), + color: Color(0xFF006CFF), + onPressed: _isGroupNameEmpty + ? null + : () async { + final groupName = _groupNameController.text; + final client = _channel.client; + _channel.extraData = { + 'members': [ + client.state.user.id, + ..._selectedUsers.map((e) => e.id), + ], + 'name': groupName, + }; + await _channel.watch(); + Navigator.of(context) + ..pop() + ..pushReplacement( + MaterialPageRoute( + builder: (context) { + return StreamChannel( + child: ChannelPage(), + channel: _channel, + ); + }, + ), + ); + }, + ), + ), + ], + ), + body: Column( + children: [ + Container( + width: double.maxFinite, + color: Colors.grey.shade50, + child: Padding( + padding: const EdgeInsets.symmetric( + vertical: 8, + horizontal: 8, + ), + child: Text( + '$_totalUsers ${_totalUsers > 1 ? 'Members' : 'Member'}', + style: TextStyle( + fontWeight: FontWeight.w500, + ), + ), + ), + ), + Expanded( + child: ListView.separated( + itemCount: _selectedUsers.length + 1, + separatorBuilder: (_, __) => Container( + height: 1, + color: Theme.of(context).brightness == Brightness.dark + ? Colors.white.withOpacity(0.1) + : Colors.black.withOpacity(0.1), + ), + itemBuilder: (_, index) { + if (index == _selectedUsers.length) { + return Container( + height: 1, + color: Theme.of(context).brightness == Brightness.dark + ? Colors.white.withOpacity(0.1) + : Colors.black.withOpacity(0.1), + ); + } + 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: Colors.black, + ), + padding: const EdgeInsets.all(0), + splashRadius: 24, + onPressed: () { + setState(() { + _selectedUsers.remove(user); + }); + if (_selectedUsers.isEmpty) { + Navigator.pop(context); + } + }, + ), + ); + }, + ), + ), + ], + ), + ); + } +} diff --git a/example/lib/main.dart b/example/lib/main.dart index e2a8a5f7..7b547659 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -1,4 +1,3 @@ -import 'dart:async'; import 'dart:io'; import 'package:example/choose_user_page.dart'; @@ -8,8 +7,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; -import 'chips_input_text_field.dart'; -import 'neumorphic_button.dart'; +import 'new_chat_screen.dart'; +import 'new_group_chat_screen.dart'; import 'notifications_service.dart'; void main() async { @@ -269,820 +268,3 @@ class ThreadPage extends StatelessWidget { ); } } - -class NewChatScreen extends StatefulWidget { - @override - _NewChatScreenState createState() => _NewChatScreenState(); -} - -class _NewChatScreenState extends State { - final _chipInputTextFieldStateKey = - GlobalKey>(); - - TextEditingController _controller; - - ChipInputTextFieldState get _chipInputTextFieldState => - _chipInputTextFieldStateKey.currentState; - - String _userNameQuery = ''; - - final _selectedUsers = {}; - - final _searchFocusNode = FocusNode(); - final _messageInputFocusNode = FocusNode(); - - bool _isSearchActive = false; - - Channel channel; - - Timer _debounce; - - bool _showUserList = true; - - void _userNameListener() { - if (_debounce?.isActive ?? false) _debounce.cancel(); - _debounce = Timer(const Duration(milliseconds: 350), () { - if (mounted) - setState(() { - _userNameQuery = _controller.text; - _isSearchActive = _userNameQuery.isNotEmpty; - }); - }); - } - - @override - void initState() { - super.initState(); - channel = StreamChat.of(context).client.channel('messaging'); - _controller = TextEditingController()..addListener(_userNameListener); - - _searchFocusNode.addListener(() async { - if (_searchFocusNode.hasFocus && !_showUserList) { - if (channel.extraData['draft'] == true) { - await channel.stopWatching(); - channel.dispose(); - channel.client.state.channels.remove(channel.cid); - } - setState(() { - _showUserList = true; - }); - } - }); - - _messageInputFocusNode.addListener(() async { - if (_messageInputFocusNode.hasFocus && _selectedUsers.isNotEmpty) { - final chatState = StreamChat.of(context); - - channel = chatState.client.channel( - 'messaging', - extraData: { - 'members': [ - ..._selectedUsers.map((e) => e.id), - chatState.user.id, - ], - 'draft': true, - }, - ); - - if (!chatState.client.state.channels.containsKey(channel.cid)) { - await channel.watch(); - } - - setState(() { - _showUserList = false; - }); - } - }); - } - - @override - void dispose() { - _searchFocusNode.dispose(); - _messageInputFocusNode.dispose(); - _controller?.clear(); - _controller?.removeListener(_userNameListener); - _controller?.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return Scaffold( - backgroundColor: Color.fromRGBO(252, 252, 252, 1), - appBar: AppBar( - elevation: 0, - backgroundColor: Colors.white, - leading: const StreamBackButton(), - title: Text( - 'New Chat', - style: TextStyle( - color: Colors.black, - fontSize: 16, - ), - ), - 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 Stack( - alignment: AlignmentDirectional.centerStart, - children: [ - Container( - decoration: BoxDecoration( - color: Colors.black.withOpacity(0.05), - borderRadius: BorderRadius.circular(12), - ), - padding: const EdgeInsets.only(left: 24), - child: Padding( - padding: const EdgeInsets.fromLTRB(8, 4, 12, 4), - child: Text( - user.name, - style: TextStyle(color: Colors.black), - ), - ), - ), - UserAvatar( - user: user, - constraints: BoxConstraints.tightFor( - height: 24, - width: 24, - ), - ), - ], - ); - }, - onChipAdded: (user) { - setState(() => _selectedUsers.add(user)); - }, - onChipRemoved: (user) { - setState(() => _selectedUsers.remove(user)); - }, - ), - if (!_isSearchActive && !_selectedUsers.isNotEmpty) - Container( - child: InkWell( - onTap: () { - Navigator.push( - context, - MaterialPageRoute(builder: (_) => NewGroupChatScreen()), - ); - }, - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 8), - child: Row( - children: [ - NeumorphicButton( - child: Icon( - StreamIcons.group, - color: Color(0xFF006CFF), - ), - ), - SizedBox(width: 8), - Text( - 'Create a Group', - style: TextStyle( - fontWeight: FontWeight.bold, - fontSize: 16, - ), - ), - ], - ), - ), - ), - ), - if (_showUserList) - Container( - width: double.maxFinite, - decoration: BoxDecoration( - gradient: LinearGradient( - begin: Alignment.centerLeft, - end: Alignment.centerRight, - colors: [ - Colors.black.withOpacity(0.02), - Colors.white.withOpacity(0.05), - ], - stops: [0, 1], - ), - ), - child: Padding( - padding: const EdgeInsets.symmetric( - vertical: 8, - horizontal: 8, - ), - child: Text( - _isSearchActive - ? "Matches for \"$_userNameQuery\"" - : 'On the platform', - style: TextStyle( - color: Colors.black.withOpacity(0.5), - ), - ), - ), - ), - Expanded( - child: _showUserList - ? 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: Icon( - StreamIcons.search, - size: 96, - color: Colors.grey, - ), - ), - Text( - 'No user matches these keywords...'), - ], - ), - ), - ), - ); - }, - ); - }, - ), - ) - : MessageListView(), - ), - MessageInput( - focusNode: _messageInputFocusNode, - onMessageSent: (m) { - if (!m.isEphemeral) { - _updateChannelAndNavigate(context); - } else { - channel.on('message.new').first.then((_) { - _updateChannelAndNavigate(context); - }); - } - }, - ), - ], - ), - ), - ); - } - - void _updateChannelAndNavigate(BuildContext context) { - channel.update({ - 'draft': false, - }); - Navigator.pushReplacement( - context, - MaterialPageRoute( - builder: (context) { - return StreamChannel( - child: ChannelPage(), - channel: channel, - ); - }, - ), - ); - } -} - -class NewGroupChatScreen extends StatefulWidget { - @override - _NewGroupChatScreenState createState() => _NewGroupChatScreenState(); -} - -class _NewGroupChatScreenState extends State { - TextEditingController _controller; - - String _userNameQuery = ''; - - final _selectedUsers = {}; - - bool _isSearchActive = false; - - Timer _debounce; - - void _userNameListener() { - if (_debounce?.isActive ?? false) _debounce.cancel(); - _debounce = Timer(const Duration(milliseconds: 350), () { - if (mounted) - setState(() { - _userNameQuery = _controller.text; - _isSearchActive = _userNameQuery.isNotEmpty; - }); - }); - } - - @override - void initState() { - super.initState(); - _controller = TextEditingController()..addListener(_userNameListener); - } - - @override - void dispose() { - _controller?.clear(); - _controller?.removeListener(_userNameListener); - _controller?.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return Scaffold( - backgroundColor: Color.fromRGBO(252, 252, 252, 1), - appBar: AppBar( - elevation: 1, - backgroundColor: Colors.white, - leading: const StreamBackButton(), - title: Text( - 'Add Group Members', - style: TextStyle( - color: Colors.black, - fontSize: 16, - ), - ), - centerTitle: true, - actions: [ - if (_selectedUsers.isNotEmpty) - IconButton( - icon: Icon( - StreamIcons.arrow_right, - color: Color(0xFF006CFF), - ), - onPressed: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (_) => GroupChatDetailsScreen( - selectedUsers: _selectedUsers.toList(growable: false), - ), - ), - ); - }, - ) - ], - ), - body: UsersBloc( - child: Column( - children: [ - Container( - height: 36, - decoration: BoxDecoration( - color: Colors.white, - border: Border.all( - color: Colors.grey.shade300, - ), - borderRadius: BorderRadius.circular(24), - ), - margin: const EdgeInsets.symmetric( - vertical: 8, - horizontal: 8, - ), - child: TextField( - controller: _controller, - decoration: InputDecoration( - prefixIcon: Icon( - StreamIcons.search, - color: Colors.black, - size: 24, - ), - hintText: 'Search', - hintStyle: TextStyle( - color: Colors.black.withOpacity(0.5), - fontSize: 14, - ), - contentPadding: const EdgeInsets.all(0), - border: OutlineInputBorder( - borderSide: BorderSide.none, - borderRadius: BorderRadius.circular(24), - ), - ), - ), - ), - if (_selectedUsers.isNotEmpty) - 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: Colors.white, - shape: BoxShape.circle, - border: Border.all( - color: Colors.grey.shade100, - ), - ), - child: Padding( - padding: const EdgeInsets.all(0.0), - child: Icon( - StreamIcons.close, - size: 24, - ), - ), - ), - ), - ) - ], - ), - SizedBox(height: 4), - Text( - user.name.split(' ')[0], - style: TextStyle( - fontWeight: FontWeight.bold, - fontSize: 12, - ), - ), - ], - ); - }, - ), - ), - Container( - width: double.maxFinite, - decoration: BoxDecoration( - gradient: LinearGradient( - begin: Alignment.centerLeft, - end: Alignment.centerRight, - colors: [ - Colors.black.withOpacity(0.02), - Colors.white.withOpacity(0.05), - ], - stops: [0, 1], - ), - ), - child: Padding( - padding: const EdgeInsets.symmetric( - vertical: 8, - horizontal: 8, - ), - child: Text( - _isSearchActive - ? 'Matches for \"$_userNameQuery\"' - : 'On the platform', - style: TextStyle( - color: Colors.black.withOpacity(0.5), - ), - ), - ), - ), - Expanded( - child: UserListView( - selectedUsers: _selectedUsers, - 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, - ), - child: Center( - child: Column( - children: [ - Padding( - padding: const EdgeInsets.all(24), - child: Icon( - StreamIcons.search, - size: 96, - color: Colors.grey, - ), - ), - Text('No user matches these keywords...'), - ], - ), - ), - ), - ); - }, - ); - }, - ), - ), - ], - ), - ), - ); - } -} - -class GroupChatDetailsScreen extends StatefulWidget { - final List selectedUsers; - - const GroupChatDetailsScreen({ - Key key, - @required this.selectedUsers, - }) : super(key: key); - - @override - _GroupChatDetailsScreenState createState() => _GroupChatDetailsScreenState(); -} - -class _GroupChatDetailsScreenState extends State { - final _selectedUsers = []; - - TextEditingController _groupNameController; - - Channel _channel; - - bool _isGroupNameEmpty = true; - - int get _totalUsers => _selectedUsers.length; - - void _groupNameListener() { - final name = _groupNameController.text; - if (mounted) { - setState(() { - _isGroupNameEmpty = name.isEmpty; - }); - } - } - - @override - void initState() { - super.initState(); - _channel = StreamChat.of(context).client.channel('messaging'); - _selectedUsers.addAll(widget.selectedUsers); - _groupNameController = TextEditingController() - ..addListener(_groupNameListener); - } - - @override - void dispose() { - _groupNameController?.clear(); - _groupNameController?.removeListener(_groupNameListener); - _groupNameController?.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return Scaffold( - backgroundColor: Color.fromRGBO(252, 252, 252, 1), - appBar: AppBar( - elevation: 1, - backgroundColor: Colors.white, - leading: const StreamBackButton(), - title: Text( - 'Name of Group Chat', - style: TextStyle( - color: Colors.black, - fontSize: 16, - ), - ), - centerTitle: true, - bottom: PreferredSize( - preferredSize: Size.fromHeight(kToolbarHeight), - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 18, horizontal: 16), - child: Row( - children: [ - Text( - 'NAME', - style: TextStyle( - fontSize: 12, - color: Colors.black.withOpacity(0.5), - ), - ), - SizedBox(width: 16), - Expanded( - child: TextField( - controller: _groupNameController, - decoration: InputDecoration( - isDense: true, - border: InputBorder.none, - focusedBorder: InputBorder.none, - enabledBorder: InputBorder.none, - errorBorder: InputBorder.none, - disabledBorder: InputBorder.none, - contentPadding: const EdgeInsets.all(0), - hintText: 'Choose a group chat name', - hintStyle: TextStyle( - fontSize: 14, color: Colors.black.withOpacity(.5)), - ), - ), - ), - ], - ), - ), - ), - actions: [ - NeumorphicButton( - child: IconButton( - padding: const EdgeInsets.all(0), - icon: Icon(StreamIcons.check), - color: Color(0xFF006CFF), - onPressed: _isGroupNameEmpty - ? null - : () async { - final groupName = _groupNameController.text; - final client = _channel.client; - _channel.extraData = { - 'members': [ - client.state.user.id, - ..._selectedUsers.map((e) => e.id), - ], - 'name': groupName, - }; - await _channel.watch(); - Navigator.of(context) - ..pop() - ..pushReplacement( - MaterialPageRoute( - builder: (context) { - return StreamChannel( - child: ChannelPage(), - channel: _channel, - ); - }, - ), - ); - }, - ), - ), - ], - ), - body: Column( - children: [ - Container( - width: double.maxFinite, - color: Colors.grey.shade50, - child: Padding( - padding: const EdgeInsets.symmetric( - vertical: 8, - horizontal: 8, - ), - child: Text( - '$_totalUsers ${_totalUsers > 1 ? 'Members' : 'Member'}', - style: TextStyle( - fontWeight: FontWeight.w500, - ), - ), - ), - ), - Expanded( - child: ListView.separated( - itemCount: _selectedUsers.length + 1, - separatorBuilder: (_, __) => Container( - height: 1, - color: Theme.of(context).brightness == Brightness.dark - ? Colors.white.withOpacity(0.1) - : Colors.black.withOpacity(0.1), - ), - itemBuilder: (_, index) { - if (index == _selectedUsers.length) { - return Container( - height: 1, - color: Theme.of(context).brightness == Brightness.dark - ? Colors.white.withOpacity(0.1) - : Colors.black.withOpacity(0.1), - ); - } - 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: Colors.black, - ), - padding: const EdgeInsets.all(0), - splashRadius: 24, - onPressed: () { - setState(() { - _selectedUsers.remove(user); - }); - if (_selectedUsers.isEmpty) { - Navigator.pop(context); - } - }, - ), - ); - }, - ), - ), - ], - ), - ); - } -} diff --git a/example/lib/new_chat_screen.dart b/example/lib/new_chat_screen.dart new file mode 100644 index 00000000..286f9a36 --- /dev/null +++ b/example/lib/new_chat_screen.dart @@ -0,0 +1,331 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +import 'chips_input_text_field.dart'; +import 'main.dart'; +import 'neumorphic_button.dart'; +import 'new_group_chat_screen.dart'; + +class NewChatScreen extends StatefulWidget { + @override + _NewChatScreenState createState() => _NewChatScreenState(); +} + +class _NewChatScreenState extends State { + final _chipInputTextFieldStateKey = + GlobalKey>(); + + TextEditingController _controller; + + ChipInputTextFieldState get _chipInputTextFieldState => + _chipInputTextFieldStateKey.currentState; + + String _userNameQuery = ''; + + final _selectedUsers = {}; + + final _searchFocusNode = FocusNode(); + final _messageInputFocusNode = FocusNode(); + + bool _isSearchActive = false; + + Channel channel; + + Timer _debounce; + + bool _showUserList = true; + + void _userNameListener() { + if (_debounce?.isActive ?? false) _debounce.cancel(); + _debounce = Timer(const Duration(milliseconds: 350), () { + if (mounted) + setState(() { + _userNameQuery = _controller.text; + _isSearchActive = _userNameQuery.isNotEmpty; + }); + }); + } + + @override + void initState() { + super.initState(); + channel = StreamChat.of(context).client.channel('messaging'); + _controller = TextEditingController()..addListener(_userNameListener); + + _searchFocusNode.addListener(() async { + if (_searchFocusNode.hasFocus && !_showUserList) { + if (channel.extraData['draft'] == true) { + await channel.stopWatching(); + channel.dispose(); + channel.client.state.channels.remove(channel.cid); + } + setState(() { + _showUserList = true; + }); + } + }); + + _messageInputFocusNode.addListener(() async { + if (_messageInputFocusNode.hasFocus && _selectedUsers.isNotEmpty) { + final chatState = StreamChat.of(context); + + channel = chatState.client.channel( + 'messaging', + extraData: { + 'members': [ + ..._selectedUsers.map((e) => e.id), + chatState.user.id, + ], + 'draft': true, + }, + ); + + if (!chatState.client.state.channels.containsKey(channel.cid)) { + await channel.watch(); + } + + setState(() { + _showUserList = false; + }); + } + }); + } + + @override + void dispose() { + _searchFocusNode.dispose(); + _messageInputFocusNode.dispose(); + _controller?.clear(); + _controller?.removeListener(_userNameListener); + _controller?.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Color.fromRGBO(252, 252, 252, 1), + appBar: AppBar( + elevation: 0, + backgroundColor: Colors.white, + leading: const StreamBackButton(), + title: Text( + 'New Chat', + style: TextStyle( + color: Colors.black, + fontSize: 16, + ), + ), + 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 Stack( + alignment: AlignmentDirectional.centerStart, + children: [ + Container( + decoration: BoxDecoration( + color: Colors.black.withOpacity(0.05), + borderRadius: BorderRadius.circular(12), + ), + padding: const EdgeInsets.only(left: 24), + child: Padding( + padding: const EdgeInsets.fromLTRB(8, 4, 12, 4), + child: Text( + user.name, + style: TextStyle(color: Colors.black), + ), + ), + ), + UserAvatar( + user: user, + constraints: BoxConstraints.tightFor( + height: 24, + width: 24, + ), + ), + ], + ); + }, + onChipAdded: (user) { + setState(() => _selectedUsers.add(user)); + }, + onChipRemoved: (user) { + setState(() => _selectedUsers.remove(user)); + }, + ), + if (!_isSearchActive && !_selectedUsers.isNotEmpty) + Container( + child: InkWell( + onTap: () { + Navigator.push( + context, + MaterialPageRoute(builder: (_) => NewGroupChatScreen()), + ); + }, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 8), + child: Row( + children: [ + NeumorphicButton( + child: Icon( + StreamIcons.group, + color: Color(0xFF006CFF), + ), + ), + SizedBox(width: 8), + Text( + 'Create a Group', + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 16, + ), + ), + ], + ), + ), + ), + ), + if (_showUserList) + Container( + width: double.maxFinite, + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.centerLeft, + end: Alignment.centerRight, + colors: [ + Colors.black.withOpacity(0.02), + Colors.white.withOpacity(0.05), + ], + stops: [0, 1], + ), + ), + child: Padding( + padding: const EdgeInsets.symmetric( + vertical: 8, + horizontal: 8, + ), + child: Text( + _isSearchActive + ? "Matches for \"$_userNameQuery\"" + : 'On the platform', + style: TextStyle( + color: Colors.black.withOpacity(0.5), + ), + ), + ), + ), + Expanded( + child: _showUserList + ? 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: Icon( + StreamIcons.search, + size: 96, + color: Colors.grey, + ), + ), + Text( + 'No user matches these keywords...'), + ], + ), + ), + ), + ); + }, + ); + }, + ), + ) + : MessageListView(), + ), + MessageInput( + focusNode: _messageInputFocusNode, + onMessageSent: (m) { + if (!m.isEphemeral) { + _updateChannelAndNavigate(context); + } else { + channel.on('message.new').first.then((_) { + _updateChannelAndNavigate(context); + }); + } + }, + ), + ], + ), + ), + ); + } + + void _updateChannelAndNavigate(BuildContext context) { + channel.update({ + 'draft': false, + }); + Navigator.pushReplacement( + context, + MaterialPageRoute( + builder: (context) { + return StreamChannel( + child: ChannelPage(), + channel: channel, + ); + }, + ), + ); + } +} diff --git a/example/lib/new_group_chat_screen.dart b/example/lib/new_group_chat_screen.dart new file mode 100644 index 00000000..b75e7510 --- /dev/null +++ b/example/lib/new_group_chat_screen.dart @@ -0,0 +1,285 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +import 'group_chat_details_screen.dart'; + +class NewGroupChatScreen extends StatefulWidget { + @override + _NewGroupChatScreenState createState() => _NewGroupChatScreenState(); +} + +class _NewGroupChatScreenState extends State { + TextEditingController _controller; + + String _userNameQuery = ''; + + final _selectedUsers = {}; + + bool _isSearchActive = false; + + Timer _debounce; + + void _userNameListener() { + if (_debounce?.isActive ?? false) _debounce.cancel(); + _debounce = Timer(const Duration(milliseconds: 350), () { + if (mounted) + setState(() { + _userNameQuery = _controller.text; + _isSearchActive = _userNameQuery.isNotEmpty; + }); + }); + } + + @override + void initState() { + super.initState(); + _controller = TextEditingController()..addListener(_userNameListener); + } + + @override + void dispose() { + _controller?.clear(); + _controller?.removeListener(_userNameListener); + _controller?.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Color.fromRGBO(252, 252, 252, 1), + appBar: AppBar( + elevation: 1, + backgroundColor: Colors.white, + leading: const StreamBackButton(), + title: Text( + 'Add Group Members', + style: TextStyle( + color: Colors.black, + fontSize: 16, + ), + ), + centerTitle: true, + actions: [ + if (_selectedUsers.isNotEmpty) + IconButton( + icon: Icon( + StreamIcons.arrow_right, + color: Color(0xFF006CFF), + ), + onPressed: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (_) => GroupChatDetailsScreen( + selectedUsers: _selectedUsers.toList(growable: false), + ), + ), + ); + }, + ) + ], + ), + body: UsersBloc( + child: Column( + children: [ + Container( + height: 36, + decoration: BoxDecoration( + color: Colors.white, + border: Border.all( + color: Colors.grey.shade300, + ), + borderRadius: BorderRadius.circular(24), + ), + margin: const EdgeInsets.symmetric( + vertical: 8, + horizontal: 8, + ), + child: TextField( + controller: _controller, + decoration: InputDecoration( + prefixIcon: Icon( + StreamIcons.search, + color: Colors.black, + size: 24, + ), + hintText: 'Search', + hintStyle: TextStyle( + color: Colors.black.withOpacity(0.5), + fontSize: 14, + ), + contentPadding: const EdgeInsets.all(0), + border: OutlineInputBorder( + borderSide: BorderSide.none, + borderRadius: BorderRadius.circular(24), + ), + ), + ), + ), + if (_selectedUsers.isNotEmpty) + 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: Colors.white, + shape: BoxShape.circle, + border: Border.all( + color: Colors.grey.shade100, + ), + ), + child: Padding( + padding: const EdgeInsets.all(0.0), + child: Icon( + StreamIcons.close, + size: 24, + ), + ), + ), + ), + ) + ], + ), + SizedBox(height: 4), + Text( + user.name.split(' ')[0], + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 12, + ), + ), + ], + ); + }, + ), + ), + Container( + width: double.maxFinite, + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.centerLeft, + end: Alignment.centerRight, + colors: [ + Colors.black.withOpacity(0.02), + Colors.white.withOpacity(0.05), + ], + stops: [0, 1], + ), + ), + child: Padding( + padding: const EdgeInsets.symmetric( + vertical: 8, + horizontal: 8, + ), + child: Text( + _isSearchActive + ? 'Matches for \"$_userNameQuery\"' + : 'On the platform', + style: TextStyle( + color: Colors.black.withOpacity(0.5), + ), + ), + ), + ), + Expanded( + child: UserListView( + selectedUsers: _selectedUsers, + 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, + ), + child: Center( + child: Column( + children: [ + Padding( + padding: const EdgeInsets.all(24), + child: Icon( + StreamIcons.search, + size: 96, + color: Colors.grey, + ), + ), + Text('No user matches these keywords...'), + ], + ), + ), + ), + ); + }, + ); + }, + ), + ), + ], + ), + ), + ); + } +} diff --git a/example/pubspec.yaml b/example/pubspec.yaml index 0fe6a0ad..9ded2435 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -1,6 +1,6 @@ name: example description: A new Flutter project. -version: 1.0.62+64 +version: 1.0.63+65 environment: sdk: ">=2.2.2 <3.0.0" From 4b317d719c80e326a4e641234315b574860280c1 Mon Sep 17 00:00:00 2001 From: xsahil03x Date: Mon, 23 Nov 2020 12:57:20 +0530 Subject: [PATCH 37/40] Refactor UserListView to support (crossAxisCount > 1) i.e: GridViews --- lib/src/user_avatar.dart | 75 ++++++++++++++++++----------- lib/src/user_list_view.dart | 94 ++++++++++++++++++++++++++++++++++--- 2 files changed, 135 insertions(+), 34 deletions(-) diff --git a/lib/src/user_avatar.dart b/lib/src/user_avatar.dart index 566ef603..2e1f3e0e 100644 --- a/lib/src/user_avatar.dart +++ b/lib/src/user_avatar.dart @@ -11,9 +11,13 @@ class UserAvatar extends StatelessWidget { this.constraints, this.onlineIndicatorConstraints, this.onTap, + this.onLongPress, this.showOnlineStatus = true, this.borderRadius, this.onlineIndicatorAlignment = Alignment.topRight, + this.selected = false, + this.selectionColor = const Color(0xFF006CFF), + this.selectionThickness = 4, }) : super(key: key); final User user; @@ -22,7 +26,11 @@ class UserAvatar extends StatelessWidget { final BorderRadius borderRadius; final BoxConstraints onlineIndicatorConstraints; final void Function(User) onTap; + final void Function(User) onLongPress; final bool showOnlineStatus; + final bool selected; + final Color selectionColor; + final double selectionThickness; @override Widget build(BuildContext context) { @@ -30,37 +38,48 @@ class UserAvatar extends StatelessWidget { user.extraData['image'] != null && user.extraData['image'] != ''; final streamChatTheme = StreamChatTheme.of(context); + + Widget avatar = ClipRRect( + borderRadius: borderRadius ?? + streamChatTheme.ownMessageTheme.avatarTheme.borderRadius, + child: Container( + constraints: constraints ?? + streamChatTheme.ownMessageTheme.avatarTheme.constraints, + decoration: BoxDecoration( + color: streamChatTheme.accentColor, + ), + child: hasImage + ? CachedNetworkImage( + filterQuality: FilterQuality.high, + imageUrl: user.extraData['image'], + errorWidget: (_, __, ___) { + return streamChatTheme.defaultUserImage(context, user); + }, + fit: BoxFit.cover, + ) + : streamChatTheme.defaultUserImage(context, user), + ), + ); + if (selected) { + avatar = ClipRRect( + borderRadius: (borderRadius ?? + streamChatTheme.ownMessageTheme.avatarTheme.borderRadius) + + BorderRadius.circular(selectionThickness), + child: Container( + color: selectionColor, + child: Padding( + padding: EdgeInsets.all(selectionThickness), + child: avatar, + ), + ), + ); + } return GestureDetector( - onTap: onTap != null - ? () { - if (onTap != null) { - onTap(user); - } - } - : null, + onTap: onTap != null ? () => onTap(user) : null, + onLongPress: onLongPress != null ? () => onLongPress(user) : null, child: Stack( children: [ - ClipRRect( - borderRadius: borderRadius ?? - streamChatTheme.ownMessageTheme.avatarTheme.borderRadius, - child: Container( - constraints: constraints ?? - streamChatTheme.ownMessageTheme.avatarTheme.constraints, - decoration: BoxDecoration( - color: streamChatTheme.accentColor, - ), - child: hasImage - ? CachedNetworkImage( - filterQuality: FilterQuality.high, - imageUrl: user.extraData['image'], - errorWidget: (_, __, ___) { - return streamChatTheme.defaultUserImage(context, user); - }, - fit: BoxFit.cover, - ) - : streamChatTheme.defaultUserImage(context, user), - ), - ), + avatar, if (showOnlineStatus && user.online == true) Positioned.fill( child: Align( diff --git a/lib/src/user_list_view.dart b/lib/src/user_list_view.dart index 0f7b969e..51c645c3 100644 --- a/lib/src/user_list_view.dart +++ b/lib/src/user_list_view.dart @@ -65,7 +65,12 @@ class UserListView extends StatefulWidget { this.swipeToAction = false, this.pullToRefresh = true, this.groupAlphabetically = false, - }) : super(key: key); + this.crossAxisCount = 1, + }) : assert( + crossAxisCount == 1 || groupAlphabetically == false, + 'Cannot group alphabetically when crossAxisCount > 1', + ), + super(key: key); /// The builder that will be used in case of error final Widget Function(Error error) errorBuilder; @@ -130,6 +135,8 @@ class UserListView extends StatefulWidget { /// defaults to false final bool groupAlphabetically; + final int crossAxisCount; + @override _UserListViewState createState() => _UserListViewState(); } @@ -138,6 +145,8 @@ class _UserListViewState extends State with WidgetsBindingObserver { final ScrollController _scrollController = ScrollController(); + bool get _isListView => widget.crossAxisCount == 1; + @override void initState() { super.initState(); @@ -317,19 +326,40 @@ class _UserListViewState extends State ); } - return ListView.custom( + if (_isListView) { + return ListView.custom( + physics: AlwaysScrollableScrollPhysics(), + controller: _scrollController, + childrenDelegate: SliverChildBuilderDelegate( + (context, i) { + return _listItemBuilder(context, i, items); + }, + childCount: (items.length * 2) + 1, + findChildIndexCallback: (key) { + final ValueKey valueKey = key; + final index = + items.indexWhere((item) => item.key == valueKey.value); + return index != -1 ? (index * 2) : null; + }, + ), + ); + } + return GridView.custom( + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: widget.crossAxisCount, + ), physics: AlwaysScrollableScrollPhysics(), controller: _scrollController, childrenDelegate: SliverChildBuilderDelegate( (context, i) { - return _itemBuilder(context, i, items); + return _gridItemBuilder(context, i, items); }, - childCount: (items.length * 2) + 1, + childCount: items.length, findChildIndexCallback: (key) { final ValueKey valueKey = key; final index = items.indexWhere((item) => item.key == valueKey.value); - return index != -1 ? (index * 2) : null; + return index != -1 ? index : null; }, ), ); @@ -337,7 +367,7 @@ class _UserListViewState extends State ); } - Widget _itemBuilder(BuildContext context, int i, List items) { + Widget _listItemBuilder(BuildContext context, int i, List items) { if (i % 2 != 0) { if (widget.separatorBuilder != null) { return widget.separatorBuilder(context, i); @@ -388,6 +418,58 @@ class _UserListViewState extends State } } + Widget _gridItemBuilder(BuildContext context, int i, List items) { + final usersProvider = UsersBloc.of(context); + if (i < items.length) { + final item = items[i]; + return item.when( + headerItem: (_) => Offstage(), + userItem: (user) { + final selected = widget.selectedUsers?.contains(user) ?? false; + return Container( + key: ValueKey('USER-${user.id}'), + child: widget.userItemBuilder != null + ? widget.userItemBuilder(context, user, selected) + : Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + UserAvatar( + user: user, + borderRadius: BorderRadius.circular(32), + selected: selected, + constraints: BoxConstraints.tightFor( + height: 64, + width: 64, + ), + onTap: (user) => + widget.onUserTap(user, widget.userWidget), + onLongPress: widget.onUserLongPress, + ), + SizedBox(height: 4), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 8), + child: Text( + user.name, + textAlign: TextAlign.center, + maxLines: 2, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 12, + ), + ), + ), + ], + ), + ); + }, + ); + } else { + return _buildQueryProgressIndicator(context, usersProvider); + } + } + Widget _buildQueryProgressIndicator(context, UsersBlocState usersProvider) { return StreamBuilder( stream: usersProvider.queryUsersLoading, From 06037acede27eb596fd5f2dd095dbba3f01413ce Mon Sep 17 00:00:00 2001 From: xsahil03x Date: Mon, 23 Nov 2020 13:48:47 +0530 Subject: [PATCH 38/40] [UserListView] Add docComment --- lib/src/user_list_view.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/src/user_list_view.dart b/lib/src/user_list_view.dart index 51c645c3..63285d8b 100644 --- a/lib/src/user_list_view.dart +++ b/lib/src/user_list_view.dart @@ -135,6 +135,7 @@ class UserListView extends StatefulWidget { /// defaults to false final bool groupAlphabetically; + /// The number of children in the cross axis. final int crossAxisCount; @override From e87c94586386cea4bf3c8ae006941486ee715e1a Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Mon, 23 Nov 2020 10:02:30 +0100 Subject: [PATCH 39/40] add id for new groups --- example/lib/group_chat_details_screen.dart | 17 ++++++++--------- example/pubspec.yaml | 3 ++- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/example/lib/group_chat_details_screen.dart b/example/lib/group_chat_details_screen.dart index 8ba8c71f..6aff5711 100644 --- a/example/lib/group_chat_details_screen.dart +++ b/example/lib/group_chat_details_screen.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; +import 'package:uuid/uuid.dart'; import 'main.dart'; import 'neumorphic_button.dart'; @@ -21,8 +22,6 @@ class _GroupChatDetailsScreenState extends State { TextEditingController _groupNameController; - Channel _channel; - bool _isGroupNameEmpty = true; int get _totalUsers => _selectedUsers.length; @@ -39,7 +38,6 @@ class _GroupChatDetailsScreenState extends State { @override void initState() { super.initState(); - _channel = StreamChat.of(context).client.channel('messaging'); _selectedUsers.addAll(widget.selectedUsers); _groupNameController = TextEditingController() ..addListener(_groupNameListener); @@ -47,8 +45,8 @@ class _GroupChatDetailsScreenState extends State { @override void dispose() { - _groupNameController?.clear(); _groupNameController?.removeListener(_groupNameListener); + _groupNameController?.clear(); _groupNameController?.dispose(); super.dispose(); } @@ -114,15 +112,16 @@ class _GroupChatDetailsScreenState extends State { ? null : () async { final groupName = _groupNameController.text; - final client = _channel.client; - _channel.extraData = { + 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(); + }); + await channel.watch(); Navigator.of(context) ..pop() ..pushReplacement( @@ -130,7 +129,7 @@ class _GroupChatDetailsScreenState extends State { builder: (context) { return StreamChannel( child: ChannelPage(), - channel: _channel, + channel: channel, ); }, ), diff --git a/example/pubspec.yaml b/example/pubspec.yaml index 9ded2435..574345c2 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -1,6 +1,6 @@ name: example description: A new Flutter project. -version: 1.0.63+65 +version: 1.0.64+66 environment: sdk: ">=2.2.2 <3.0.0" @@ -15,6 +15,7 @@ dependencies: flutter_svg: ^0.19.1 flutter_secure_storage: ^3.3.5 yaml: ^2.2.1 + uuid: ^2.2.2 dev_dependencies: flutter_test: From 37891ae3c53cb9ff2ca2a051770cabce541f8c0e Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Mon, 23 Nov 2020 10:30:58 +0100 Subject: [PATCH 40/40] remove chips tapping on them --- example/lib/new_chat_screen.dart | 59 ++++++++++++++++++++------------ example/pubspec.yaml | 2 +- 2 files changed, 38 insertions(+), 23 deletions(-) diff --git a/example/lib/new_chat_screen.dart b/example/lib/new_chat_screen.dart index 286f9a36..9ec2ca7e 100644 --- a/example/lib/new_chat_screen.dart +++ b/example/lib/new_chat_screen.dart @@ -131,31 +131,46 @@ class _NewChatScreenState extends State { controller: _controller, focusNode: _searchFocusNode, chipBuilder: (context, user) { - return Stack( - alignment: AlignmentDirectional.centerStart, - children: [ - Container( - decoration: BoxDecoration( - color: Colors.black.withOpacity(0.05), - borderRadius: BorderRadius.circular(12), - ), - padding: const EdgeInsets.only(left: 24), - child: Padding( - padding: const EdgeInsets.fromLTRB(8, 4, 12, 4), - child: Text( - user.name, - style: TextStyle(color: Colors.black), + return GestureDetector( + onTap: () { + _chipInputTextFieldState.removeItem(user); + }, + child: Stack( + alignment: AlignmentDirectional.centerStart, + children: [ + Container( + decoration: BoxDecoration( + color: Colors.black.withOpacity(0.05), + borderRadius: BorderRadius.circular(12), + ), + padding: const EdgeInsets.only(left: 24), + child: Padding( + padding: const EdgeInsets.fromLTRB(8, 4, 12, 4), + child: Text( + user.name, + style: TextStyle(color: Colors.black), + ), ), ), - ), - UserAvatar( - user: user, - constraints: BoxConstraints.tightFor( - height: 24, - width: 24, + Opacity( + opacity: .8, + child: UserAvatar( + showOnlineStatus: false, + user: user, + constraints: BoxConstraints.tightFor( + height: 24, + width: 24, + ), + ), ), - ), - ], + Positioned( + child: Icon( + StreamIcons.close, + color: Colors.white, + ), + ), + ], + ), ); }, onChipAdded: (user) { diff --git a/example/pubspec.yaml b/example/pubspec.yaml index 574345c2..b491fb92 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -1,6 +1,6 @@ name: example description: A new Flutter project. -version: 1.0.64+66 +version: 1.0.65+67 environment: sdk: ">=2.2.2 <3.0.0"