From 4aa2a8946a8906dc5455b8992ea84b36af512e75 Mon Sep 17 00:00:00 2001 From: Deven Joshi Date: Tue, 22 Dec 2020 17:45:08 +0530 Subject: [PATCH] feat: Added gridview for channel list view and changed implementation for image sharing --- lib/src/channel_image.dart | 6 ++ lib/src/channel_list_view.dart | 91 +++++++++++++++++---- lib/src/group_image.dart | 41 ++++++++-- lib/src/image_footer.dart | 143 ++++++++++++++++----------------- lib/src/user_avatar.dart | 3 + 5 files changed, 192 insertions(+), 92 deletions(-) diff --git a/lib/src/channel_image.dart b/lib/src/channel_image.dart index c3d76202..d79b2efa 100644 --- a/lib/src/channel_image.dart +++ b/lib/src/channel_image.dart @@ -52,6 +52,7 @@ class ChannelImage extends StatelessWidget { this.onTap, this.showOnlineStatus = true, this.borderRadius, + this.selected = false, }) : super(key: key); final BorderRadius borderRadius; @@ -67,6 +68,8 @@ class ChannelImage extends StatelessWidget { final bool showOnlineStatus; + final bool selected; + @override Widget build(BuildContext context) { final streamChat = StreamChat.of(context); @@ -99,6 +102,7 @@ class ChannelImage extends StatelessWidget { onTap(); } : null, + selected: selected, ); }); } else { @@ -111,12 +115,14 @@ class ChannelImage extends StatelessWidget { .toList(); return GroupImage( images: images, + borderRadius: borderRadius, constraints: constraints ?? StreamChatTheme.of(context) .channelPreviewTheme .avatarTheme .constraints, onTap: onTap, + selected: selected, ); } diff --git a/lib/src/channel_list_view.dart b/lib/src/channel_list_view.dart index 5dbc9f90..d5278f5e 100644 --- a/lib/src/channel_list_view.dart +++ b/lib/src/channel_list_view.dart @@ -74,6 +74,8 @@ class ChannelListView extends StatefulWidget { this.onStartChatPressed, this.swipeToAction = false, this.pullToRefresh = true, + this.crossAxisCount = 1, + this.selectedChannels = const [], }) : super(key: key); /// The builder that will be used in case of error @@ -134,6 +136,11 @@ class ChannelListView extends StatefulWidget { /// Callback used in the default empty list widget final VoidCallback onStartChatPressed; + /// The number of children in the cross axis. + final int crossAxisCount; + + final List selectedChannels; + @override _ChannelListViewState createState() => _ChannelListViewState(); } @@ -259,22 +266,35 @@ class _ChannelListViewState extends State } if (channels.isNotEmpty) { - child = ListView.custom( - physics: AlwaysScrollableScrollPhysics(), - controller: _scrollController, - childrenDelegate: SliverChildBuilderDelegate( - (context, i) { - return _itemBuilder(context, i, channels); + if (widget.crossAxisCount > 1) { + child = GridView.builder( + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: widget.crossAxisCount), + itemCount: channels.length, + physics: AlwaysScrollableScrollPhysics(), + controller: _scrollController, + itemBuilder: (context, index) { + return _gridItemBuilder(context, index, channels); }, - childCount: (channels.length * 2) + 1, - findChildIndexCallback: (key) { - final ValueKey valueKey = key; - final index = channels.indexWhere( - (channel) => 'CHANNEL-${channel.id}' == valueKey.value); - return index != -1 ? (index * 2) : null; - }, - ), - ); + ); + } else { + child = ListView.custom( + physics: AlwaysScrollableScrollPhysics(), + controller: _scrollController, + childrenDelegate: SliverChildBuilderDelegate( + (context, i) { + return _itemBuilder(context, i, channels); + }, + childCount: (channels.length * 2) + 1, + findChildIndexCallback: (key) { + final ValueKey valueKey = key; + final index = channels.indexWhere( + (channel) => 'CHANNEL-${channel.id}' == valueKey.value); + return index != -1 ? (index * 2) : null; + }, + ), + ); + } } } @@ -581,6 +601,47 @@ class _ChannelListViewState extends State } } + Widget _gridItemBuilder(BuildContext context, int i, List channels) { + var channel = channels[i]; + + var selected = widget.selectedChannels.contains(channel); + + return Container( + key: ValueKey('CHANNEL-${channel.id}'), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + ChannelImage( + channel: channel, + borderRadius: BorderRadius.circular(32), + selected: selected, + constraints: BoxConstraints.tightFor( + width: 64, + height: 64, + ), + onTap: () { + widget.onChannelTap(channel, null); + }, + ), + SizedBox(height: 7), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 8), + child: StreamChannel( + child: ChannelName( + textStyle: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w600, + ), + ), + channel: channel, + ), + ), + ], + ), + ); + } + Widget _buildQueryProgressIndicator( context, ChannelsBlocState channelsProvider, diff --git a/lib/src/group_image.dart b/lib/src/group_image.dart index 96e25f74..5f54377d 100644 --- a/lib/src/group_image.dart +++ b/lib/src/group_image.dart @@ -9,21 +9,33 @@ class GroupImage extends StatelessWidget { @required this.images, this.constraints, this.onTap, + this.borderRadius, + this.selected = false, + this.selectionColor = const Color(0xFF006CFF), + this.selectionThickness = 4, }) : super(key: key); final List images; final BoxConstraints constraints; final VoidCallback onTap; + final bool selected; + final BorderRadius borderRadius; + final Color selectionColor; + final double selectionThickness; @override Widget build(BuildContext context) { - return GestureDetector( + var avatar; + final streamChatTheme = StreamChatTheme.of(context); + + avatar = GestureDetector( onTap: onTap, child: ClipRRect( - borderRadius: StreamChatTheme.of(context) - .ownMessageTheme - .avatarTheme - .borderRadius, + borderRadius: borderRadius ?? + StreamChatTheme.of(context) + .ownMessageTheme + .avatarTheme + .borderRadius, child: Container( constraints: constraints ?? StreamChatTheme.of(context) @@ -91,5 +103,24 @@ class GroupImage extends StatelessWidget { ), ), ); + + if (selected) { + avatar = ClipRRect( + borderRadius: (borderRadius ?? + streamChatTheme.ownMessageTheme.avatarTheme.borderRadius) + + BorderRadius.circular(selectionThickness), + child: Container( + color: selectionColor, + height: 64.0, + width: 64.0, + child: Padding( + padding: EdgeInsets.all(selectionThickness), + child: avatar, + ), + ), + ); + } + + return avatar; } } diff --git a/lib/src/image_footer.dart b/lib/src/image_footer.dart index 3c37a66a..79c1f501 100644 --- a/lib/src/image_footer.dart +++ b/lib/src/image_footer.dart @@ -68,7 +68,7 @@ class _ImageFooterState extends State { String _userNameQuery; bool _isSearchActive = false; - Set _selectedUsers = {}; + List _selectedChannels = []; bool _loading = false; Timer _debounce; @@ -278,78 +278,86 @@ class _ImageFooterState extends State { ), clipBehavior: Clip.antiAlias, child: Scaffold( + resizeToAvoidBottomInset: false, body: UsersBloc( child: Column( children: [ _buildTextInputSection(modalSetState), + if (_userSearchMode) + SizedBox( + height: 22.0, + ), Expanded( - child: UserListView( - selectedUsers: _selectedUsers, - onUserTap: (user, _) { - _searchController.clear(); - if (!_selectedUsers.contains(user)) { - modalSetState(() { - _selectedUsers.add(user); - }); - } else { - modalSetState(() { - _selectedUsers.remove(user); - }); - } - }, - crossAxisCount: 4, - pagination: PaginationParams( - limit: 25, - ), - filter: { - if (_searchController.text.isNotEmpty) - 'name': { - r'$autocomplete': _userNameQuery, - }, - 'id': { - r'$ne': StreamChat.of(context).user.id, + child: ChannelsBloc( + child: ChannelListView( + selectedChannels: _selectedChannels, + onChannelTap: (user, _) { + _searchController.clear(); + if (!_selectedChannels.contains(user)) { + modalSetState(() { + _selectedChannels.add(user); + }); + } else { + modalSetState(() { + _selectedChannels.remove(user); + }); + } }, - }, - sort: [ - SortOption( - 'name', - direction: 1, + crossAxisCount: 4, + 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: StreamSvgIcon.search( - size: 96, - color: Colors.grey, + filter: { + if (_searchController.text.isNotEmpty) + 'name': { + r'$autocomplete': _userNameQuery, + }, + 'id': { + r'$ne': StreamChat.of(context).user.id, + }, + }, + sort: [ + SortOption( + 'name', + direction: 1, + ), + ], + emptyBuilder: (_) { + return LayoutBuilder( + builder: (context, viewportConstraints) { + return SingleChildScrollView( + physics: AlwaysScrollableScrollPhysics(), + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: + viewportConstraints.maxHeight, + ), + child: Center( + child: Column( + children: [ + Padding( + padding: const EdgeInsets.all(24), + child: StreamSvgIcon.search( + size: 96, + color: Colors.grey, + ), ), - ), - Text( - 'No user matches these keywords...'), - ], + Text( + 'No user matches these keywords...'), + ], + ), ), ), - ), - ); - }, - ); - }, + ); + }, + ); + }, + ), ), ), - if (_selectedUsers.isNotEmpty) + if (_selectedChannels.isNotEmpty) _buildShareTextInputSection(modalSetState), - if (!_userSearchMode && _selectedUsers.isEmpty) + if (!_userSearchMode && _selectedChannels.isEmpty) Align( alignment: Alignment.bottomCenter, child: Container( @@ -614,25 +622,16 @@ class _ImageFooterState extends State { final client = StreamChat.of(context).client; - for (var user in _selectedUsers) { - var c = client.channel('messaging', extraData: { - 'members': [ - user.id, - StreamChat.of(context).user.id, - ], - }); - - await c.watch(); - + for (var channel in _selectedChannels) { final message = Message( text: text, attachments: [attachments[widget.currentPage]], ); - await c.sendMessage(message); + await channel.sendMessage(message); } - _selectedUsers.clear(); + _selectedChannels.clear(); Navigator.pop(context); } diff --git a/lib/src/user_avatar.dart b/lib/src/user_avatar.dart index 756252cc..e698d773 100644 --- a/lib/src/user_avatar.dart +++ b/lib/src/user_avatar.dart @@ -61,12 +61,15 @@ class UserAvatar extends StatelessWidget { : streamChatTheme.defaultUserImage(context, user), ), ); + if (selected) { avatar = ClipRRect( borderRadius: (borderRadius ?? streamChatTheme.ownMessageTheme.avatarTheme.borderRadius) + BorderRadius.circular(selectionThickness), child: Container( + constraints: constraints ?? + streamChatTheme.ownMessageTheme.avatarTheme.constraints, color: selectionColor, child: Padding( padding: EdgeInsets.all(selectionThickness),