From e5b25d8d06d57896dfbbe9fc08ceb470b727f78e Mon Sep 17 00:00:00 2001 From: xsahil03x Date: Tue, 17 Nov 2020 23:32:16 +0530 Subject: [PATCH] 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';