From f2a707bc45e13cf0f546002fb8d33ee179e7a8f5 Mon Sep 17 00:00:00 2001 From: Deven Joshi Date: Mon, 14 Dec 2020 20:22:48 +0530 Subject: [PATCH] feat: Added group info screen, refactored components, added some functionality --- lib/src/channel_file_display_screen.dart | 36 ++ lib/src/channel_header.dart | 11 + lib/src/channel_media_display_screen.dart | 36 ++ lib/src/channel_preview.dart | 11 + lib/src/chat_info_screen.dart | 161 +------ lib/src/group_info_screen.dart | 502 ++++++++++++++++++++++ lib/src/option_list_tile.dart | 68 +++ 7 files changed, 681 insertions(+), 144 deletions(-) create mode 100644 lib/src/channel_file_display_screen.dart create mode 100644 lib/src/channel_media_display_screen.dart create mode 100644 lib/src/group_info_screen.dart create mode 100644 lib/src/option_list_tile.dart diff --git a/lib/src/channel_file_display_screen.dart b/lib/src/channel_file_display_screen.dart new file mode 100644 index 00000000..d34a4b87 --- /dev/null +++ b/lib/src/channel_file_display_screen.dart @@ -0,0 +1,36 @@ +import 'package:flutter/material.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +class ChannelFileDisplayScreen extends StatelessWidget { + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Colors.white, + appBar: AppBar( + brightness: Theme.of(context).brightness, + elevation: 1, + centerTitle: true, + title: Text( + 'Files', + style: TextStyle(color: Colors.black, fontSize: 16.0), + ), + leading: Center( + child: InkWell( + onTap: () { + Navigator.of(context).pop(); + }, + child: Container( + child: StreamSvgIcon.left( + color: Colors.black, + size: 24.0, + ), + width: 24.0, + height: 24.0, + ), + ), + ), + backgroundColor: StreamChatTheme.of(context).primaryColor, + ), + ); + } +} diff --git a/lib/src/channel_header.dart b/lib/src/channel_header.dart index 65f63cc4..73e44b35 100644 --- a/lib/src/channel_header.dart +++ b/lib/src/channel_header.dart @@ -9,6 +9,7 @@ import '../stream_chat_flutter.dart'; import './channel_name.dart'; import 'channel_image.dart'; import 'chat_info_screen.dart'; +import 'group_info_screen.dart'; import 'stream_channel.dart'; /// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/screenshots/channel_header.png) @@ -124,6 +125,16 @@ class ChannelHeader extends StatelessWidget implements PreferredSizeWidget { Navigator.pop(context); } } + } else { + await Navigator.push( + context, + MaterialPageRoute( + builder: (context) => StreamChannel( + channel: channel, + child: GroupInfoScreen(), + ), + ), + ); } }, ), diff --git a/lib/src/channel_media_display_screen.dart b/lib/src/channel_media_display_screen.dart new file mode 100644 index 00000000..bcad35e2 --- /dev/null +++ b/lib/src/channel_media_display_screen.dart @@ -0,0 +1,36 @@ +import 'package:flutter/material.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +class ChannelMediaDisplayScreen extends StatelessWidget { + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Colors.white, + appBar: AppBar( + brightness: Theme.of(context).brightness, + elevation: 1, + centerTitle: true, + title: Text( + 'Photos & Videos', + style: TextStyle(color: Colors.black, fontSize: 16.0), + ), + leading: Center( + child: InkWell( + onTap: () { + Navigator.of(context).pop(); + }, + child: Container( + child: StreamSvgIcon.left( + color: Colors.black, + size: 24.0, + ), + width: 24.0, + height: 24.0, + ), + ), + ), + backgroundColor: StreamChatTheme.of(context).primaryColor, + ), + ); + } +} diff --git a/lib/src/channel_preview.dart b/lib/src/channel_preview.dart index 5f58e254..ff727cfa 100644 --- a/lib/src/channel_preview.dart +++ b/lib/src/channel_preview.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; import 'package:jiffy/jiffy.dart'; import 'package:stream_chat/stream_chat.dart'; +import 'package:stream_chat_flutter/src/group_info_screen.dart'; import 'package:stream_chat_flutter/src/stream_svg_icon.dart'; import '../stream_chat_flutter.dart'; @@ -82,6 +83,16 @@ class ChannelPreview extends StatelessWidget { ), ); } + } else { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => StreamChannel( + channel: channel, + child: GroupInfoScreen(), + ), + ), + ); } }, ), diff --git a/lib/src/chat_info_screen.dart b/lib/src/chat_info_screen.dart index 0ec62c36..436ff675 100644 --- a/lib/src/chat_info_screen.dart +++ b/lib/src/chat_info_screen.dart @@ -4,6 +4,9 @@ import 'package:flutter/material.dart'; import 'package:jiffy/jiffy.dart'; import '../stream_chat_flutter.dart'; +import 'channel_file_display_screen.dart'; +import 'channel_media_display_screen.dart'; +import 'option_list_tile.dart'; /// Detail screen for a 1:1 chat correspondence class ChatInfoScreen extends StatefulWidget { @@ -74,7 +77,7 @@ class _ChatInfoScreenState extends State { SizedBox(height: 7.0), _buildConnectedTitleState(), SizedBox(height: 15.0), - _OptionListTile( + OptionListTile( title: '@${widget.user.id}', trailing: Padding( padding: const EdgeInsets.symmetric(horizontal: 8.0), @@ -124,7 +127,7 @@ class _ChatInfoScreenState extends State { StreamBuilder( stream: StreamChannel.of(context).channel.isMutedStream, builder: (context, snapshot) { - return _OptionListTile( + return OptionListTile( title: 'Mute user', leading: StreamSvgIcon.mute( size: 23.0, @@ -163,7 +166,7 @@ class _ChatInfoScreenState extends State { // ), // onTap: () {}, // ), - _OptionListTile( + OptionListTile( title: 'Photos & Videos', leading: StreamSvgIcon.pictures( size: 32.0, @@ -171,11 +174,13 @@ class _ChatInfoScreenState extends State { ), trailing: StreamSvgIcon.right(), onTap: () { - Navigator.push(context, - MaterialPageRoute(builder: (context) => _MediaDisplayScreen())); + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => ChannelMediaDisplayScreen())); }, ), - _OptionListTile( + OptionListTile( title: 'Files', leading: StreamSvgIcon.files( size: 32.0, @@ -183,11 +188,13 @@ class _ChatInfoScreenState extends State { ), trailing: StreamSvgIcon.right(), onTap: () { - Navigator.push(context, - MaterialPageRoute(builder: (context) => _FileDisplayScreen())); + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => ChannelFileDisplayScreen())); }, ), - _OptionListTile( + OptionListTile( title: 'Shared groups', leading: StreamSvgIcon.Icon_group( size: 24.0, @@ -207,7 +214,7 @@ class _ChatInfoScreenState extends State { } Widget _buildDeleteListTile() { - return _OptionListTile( + return OptionListTile( title: 'Delete', leading: StreamSvgIcon.delete( color: Colors.red, @@ -283,72 +290,6 @@ class _ChatInfoScreenState extends State { } } -class _OptionListTile extends StatelessWidget { - final String title; - final StreamSvgIcon leading; - final Widget trailing; - final VoidCallback onTap; - final Color titleColor; - - _OptionListTile({ - this.title, - this.leading, - this.trailing, - this.onTap, - this.titleColor, - }); - - @override - Widget build(BuildContext context) { - return Column( - children: [ - Container( - color: Color(0xffe6e6e6), - height: 2.0, - ), - Material( - color: Colors.white, - child: Container( - height: 56.0, - child: InkWell( - onTap: onTap, - child: Row( - children: [ - if (leading != null) - Expanded( - child: Center(child: leading), - ), - if (leading == null) - SizedBox( - width: 16.0, - ), - Expanded( - flex: 4, - child: Text( - title, - style: TextStyle( - fontWeight: FontWeight.w600, color: titleColor), - )), - Expanded( - flex: 2, - child: Padding( - padding: const EdgeInsets.only(right: 16.0), - child: Align( - alignment: Alignment.centerRight, - child: trailing ?? Container(), - ), - ), - ), - ], - ), - ), - ), - ), - ], - ); - } -} - class _SharedGroupsScreen extends StatefulWidget { final User mainUser; final User otherUser; @@ -504,71 +445,3 @@ class __SharedGroupsScreenState extends State<_SharedGroupsScreen> { ); } } - -class _MediaDisplayScreen extends StatelessWidget { - @override - Widget build(BuildContext context) { - return Scaffold( - backgroundColor: Colors.white, - appBar: AppBar( - brightness: Theme.of(context).brightness, - elevation: 1, - centerTitle: true, - title: Text( - 'Photos & Videos', - style: TextStyle(color: Colors.black, fontSize: 16.0), - ), - leading: Center( - child: InkWell( - onTap: () { - Navigator.of(context).pop(); - }, - child: Container( - child: StreamSvgIcon.left( - color: Colors.black, - size: 24.0, - ), - width: 24.0, - height: 24.0, - ), - ), - ), - backgroundColor: StreamChatTheme.of(context).primaryColor, - ), - ); - } -} - -class _FileDisplayScreen extends StatelessWidget { - @override - Widget build(BuildContext context) { - return Scaffold( - backgroundColor: Colors.white, - appBar: AppBar( - brightness: Theme.of(context).brightness, - elevation: 1, - centerTitle: true, - title: Text( - 'Files', - style: TextStyle(color: Colors.black, fontSize: 16.0), - ), - leading: Center( - child: InkWell( - onTap: () { - Navigator.of(context).pop(); - }, - child: Container( - child: StreamSvgIcon.left( - color: Colors.black, - size: 24.0, - ), - width: 24.0, - height: 24.0, - ), - ), - ), - backgroundColor: StreamChatTheme.of(context).primaryColor, - ), - ); - } -} diff --git a/lib/src/group_info_screen.dart b/lib/src/group_info_screen.dart new file mode 100644 index 00000000..ceb33dc8 --- /dev/null +++ b/lib/src/group_info_screen.dart @@ -0,0 +1,502 @@ +import 'dart:async'; + +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:jiffy/jiffy.dart'; +import 'package:stream_chat_flutter/src/option_list_tile.dart'; +import 'package:stream_chat_flutter/src/stream_svg_icon.dart'; + +import '../stream_chat_flutter.dart'; +import 'channel_file_display_screen.dart'; +import 'channel_media_display_screen.dart'; + +class GroupInfoScreen extends StatefulWidget { + @override + _GroupInfoScreenState createState() => _GroupInfoScreenState(); +} + +class _GroupInfoScreenState extends State { + Future _memberQueryFuture; + TextEditingController _nameController; + + bool _userSearchMode = false; + TextEditingController _searchController; + bool _loading = false; + String _userNameQuery = ''; + + Timer _debounce; + Function modalSetStateCallback; + + void _userNameListener() { + if (_debounce?.isActive ?? false) _debounce.cancel(); + _debounce = Timer(const Duration(milliseconds: 350), () { + if (mounted && modalSetStateCallback != null) { + modalSetStateCallback(() { + _userNameQuery = _searchController.text; + }); + } + }); + } + + @override + void initState() { + super.initState(); + var channel = StreamChannel.of(context); + + _memberQueryFuture = channel.channel.queryMembers( + filter: {}, + ); + _nameController = TextEditingController.fromValue( + TextEditingValue(text: channel.channel.extraData['name'])); + _searchController = TextEditingController()..addListener(_userNameListener); + } + + @override + Widget build(BuildContext context) { + var channel = StreamChannel.of(context); + + return Scaffold( + backgroundColor: Color(0xffdbdbdb), + appBar: AppBar( + elevation: 1.0, + toolbarHeight: 56.0, + backgroundColor: Colors.white, + leading: InkWell( + onTap: () { + Navigator.pop(context); + }, + child: Padding( + padding: const EdgeInsets.only(left: 12.0), + child: StreamSvgIcon.left(), + ), + ), + leadingWidth: 36.0, + title: Column( + children: [ + // TODO: Add member names instead of Demo (if name = null) + Text( + channel.channel.extraData['name'] ?? 'Demo', + style: TextStyle(color: Colors.black), + ), + SizedBox( + height: 3.0, + ), + FutureBuilder( + future: _memberQueryFuture, + builder: (context, snapshot) { + return Text( + '${channel.channel.memberCount} Members, ${snapshot?.data?.members?.where((e) => e.user.online)?.length ?? 0} Online', + style: TextStyle( + color: Colors.black.withOpacity(0.5), fontSize: 12.0), + ); + }), + ], + ), + centerTitle: true, + actions: [ + StreamNeumorphicButton( + child: InkWell( + onTap: () { + _buildAddUserModal(context); + }, + child: Padding( + padding: const EdgeInsets.all(8.0), + child: StreamSvgIcon.userAdd( + color: StreamChatTheme.of(context).accentColor), + ), + ), + ), + ], + ), + body: ListView( + children: [ + _buildMembers(), + Container( + height: 8.0, + color: Color(0xffdbdbdb), + ), + _buildNameTile(), + _buildOptionListTiles(), + ], + ), + ); + } + + Widget _buildMembers() { + var channel = StreamChannel.of(context); + + return FutureBuilder( + builder: (context, snapshot) { + if (snapshot.data == null) { + return Center( + child: Padding( + padding: const EdgeInsets.all(8.0), + child: CircularProgressIndicator(), + ), + ); + } + + return ListView.builder( + shrinkWrap: true, + physics: NeverScrollableScrollPhysics(), + itemCount: snapshot.data.members.length, + itemBuilder: (context, position) { + return Material( + child: Container( + height: 65.0, + child: Column( + children: [ + Row( + children: [ + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 8.0, vertical: 12.0), + child: UserAvatar( + user: snapshot.data.members[position].user, + constraints: + BoxConstraints(maxHeight: 40.0, maxWidth: 40.0), + ), + ), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + snapshot.data.members[position].user.name, + style: TextStyle(fontWeight: FontWeight.bold), + ), + SizedBox( + height: 1.0, + ), + Text( + _getLastSeen( + snapshot.data.members[position].user), + style: TextStyle( + color: Colors.black.withOpacity(0.5)), + ), + ], + ), + ), + Padding( + padding: const EdgeInsets.all(8.0), + child: Text( + snapshot.data.members[position].user.id == + channel.channel.createdBy.id + ? 'Owner' + : '', + style: + TextStyle(color: Colors.black.withOpacity(0.5)), + ), + ), + ], + ), + Container( + height: 1.0, + color: Color(0xffdbdbdb), + ), + ], + ), + ), + color: Colors.white, + ); + }, + ); + }, + future: _memberQueryFuture, + ); + } + + Widget _buildNameTile() { + return Material( + color: Colors.white, + child: Container( + height: 56.0, + alignment: Alignment.center, + child: Row( + children: [ + Padding( + padding: const EdgeInsets.all(8.0), + child: Text( + 'NAME', + style: TextStyle( + color: Colors.black.withOpacity(0.5), fontSize: 12.0), + ), + ), + SizedBox( + width: 9.0, + ), + Expanded( + child: TextField( + controller: _nameController, + cursorColor: Colors.black, + decoration: InputDecoration.collapsed( + hintText: 'Add a group name', + hintStyle: TextStyle( + fontWeight: FontWeight.bold, + color: Colors.black.withOpacity(0.5), + ), + ), + style: TextStyle( + fontWeight: FontWeight.bold, + ), + ), + ), + ], + ), + ), + ); + } + + Widget _buildOptionListTiles() { + var channel = StreamChannel.of(context); + + return Column( + children: [ + // OptionListTile( + // title: 'Notifications', + // leading: StreamSvgIcon.Icon_notification( + // size: 24.0, + // color: Colors.black.withOpacity(0.5), + // ), + // trailing: CupertinoSwitch( + // value: true, + // onChanged: (val) {}, + // ), + // onTap: () {}, + // ), + StreamBuilder( + stream: StreamChannel.of(context).channel.isMutedStream, + builder: (context, snapshot) { + return OptionListTile( + title: 'Mute group', + leading: StreamSvgIcon.mute( + size: 23.0, + color: Colors.black.withOpacity(0.5), + ), + trailing: snapshot.data == null + ? CircularProgressIndicator() + : CupertinoSwitch( + value: snapshot.data, + onChanged: (val) { + if (snapshot.data) { + channel.channel.unmute(); + } else { + channel.channel.mute(); + } + }, + ), + onTap: () {}, + ); + }), + OptionListTile( + title: 'Photos & Videos', + leading: StreamSvgIcon.pictures( + size: 32.0, + color: Colors.black.withOpacity(0.5), + ), + trailing: StreamSvgIcon.right(), + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => ChannelMediaDisplayScreen())); + }, + ), + OptionListTile( + title: 'Files', + leading: StreamSvgIcon.files( + size: 32.0, + color: Colors.black.withOpacity(0.5), + ), + trailing: StreamSvgIcon.right(), + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => ChannelFileDisplayScreen())); + }, + ), + OptionListTile( + title: 'Leave Group', + leading: StreamSvgIcon.userRemove( + size: 24.0, + color: Colors.black.withOpacity(0.5), + ), + trailing: Container( + height: 24.0, + width: 24.0, + ), + onTap: () async { + await channel.channel + .removeMembers([StreamChat.of(context).user.id]); + Navigator.pop(context); + Navigator.pop(context); + }, + ), + ], + ); + } + + void _buildAddUserModal(context) { + var channel = StreamChannel.of(context).channel; + + showDialog( + context: context, + builder: (context) { + return StatefulBuilder(builder: (context, modalSetState) { + modalSetStateCallback = modalSetState; + return Padding( + padding: EdgeInsets.only(top: 16.0, left: 8.0, right: 8.0), + child: Material( + borderRadius: BorderRadius.only( + topLeft: Radius.circular(16.0), + topRight: Radius.circular(16.0), + ), + clipBehavior: Clip.antiAlias, + child: Scaffold( + body: UsersBloc( + child: Column( + children: [ + _buildTextInputSection(modalSetState), + Expanded( + child: UserListView( + selectedUsers: {}, + onUserTap: (user, _) async { + _searchController.clear(); + + await channel.addMembers([user.id]); + Navigator.pop(context); + _memberQueryFuture = channel.queryMembers( + filter: {}, + ); + setState(() {}); + }, + crossAxisCount: 4, + pagination: PaginationParams( + limit: 25, + ), + 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...'), + ], + ), + ), + ), + ); + }, + ); + }, + ), + ), + ], + ), + ), + ), + ), + ); + }); + }, + ); + } + + Widget _buildTextInputSection(modalSetState) { + return Column( + children: [ + SizedBox( + height: 16.0, + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + SizedBox( + width: 16.0, + ), + Expanded( + child: TextField( + controller: _searchController, + cursorColor: Colors.black, + autofocus: true, + decoration: InputDecoration( + isDense: true, + prefixIconConstraints: BoxConstraints.tight(Size(36.0, 44.0)), + prefixIcon: Padding( + padding: const EdgeInsets.symmetric( + vertical: 2.0, horizontal: 6.0), + child: StreamSvgIcon.search( + color: Colors.black, + ), + ), + hintText: 'Search', + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(32.0), + borderSide: + BorderSide(color: Colors.black.withOpacity(0.08)), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(32.0), + borderSide: + BorderSide(color: Colors.black.withOpacity(0.08)), + ), + contentPadding: EdgeInsets.zero, + ), + ), + ), + SizedBox( + width: 8.0, + ), + IconButton( + icon: StreamSvgIcon.close_small( + color: Colors.black.withOpacity(0.5), + ), + onPressed: () { + Navigator.pop(context); + }, + ) + ], + ), + ], + ); + } + + String _getLastSeen(User user) { + if (user.online) { + return 'Online'; + } else { + return 'Last seen ${Jiffy(user.lastActive).fromNow()}'; + } + } +} diff --git a/lib/src/option_list_tile.dart b/lib/src/option_list_tile.dart new file mode 100644 index 00000000..c1f48b71 --- /dev/null +++ b/lib/src/option_list_tile.dart @@ -0,0 +1,68 @@ +import 'package:flutter/material.dart'; +import 'package:stream_chat_flutter/src/stream_svg_icon.dart'; + +class OptionListTile extends StatelessWidget { + final String title; + final StreamSvgIcon leading; + final Widget trailing; + final VoidCallback onTap; + final Color titleColor; + + OptionListTile({ + this.title, + this.leading, + this.trailing, + this.onTap, + this.titleColor, + }); + + @override + Widget build(BuildContext context) { + return Column( + children: [ + Container( + color: Color(0xffe6e6e6), + height: 2.0, + ), + Material( + color: Colors.white, + child: Container( + height: 56.0, + child: InkWell( + onTap: onTap, + child: Row( + children: [ + if (leading != null) + Expanded( + child: Center(child: leading), + ), + if (leading == null) + SizedBox( + width: 16.0, + ), + Expanded( + flex: 4, + child: Text( + title, + style: TextStyle( + fontWeight: FontWeight.w600, color: titleColor), + )), + Expanded( + flex: 2, + child: Padding( + padding: const EdgeInsets.only(right: 16.0), + child: Align( + alignment: Alignment.centerRight, + child: trailing ?? Container(), + ), + ), + ), + ], + ), + ), + ), + ), + ], + ); + } +}