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,