From d7a8d4f3f9111fbacf40e26bda15cefc072cb20a Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Sat, 21 Nov 2020 11:24:38 +0100 Subject: [PATCH] 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"