diff --git a/example/lib/chips_input_text_field.dart b/example/lib/chips_input_text_field.dart new file mode 100644 index 00000000..e62773f7 --- /dev/null +++ b/example/lib/chips_input_text_field.dart @@ -0,0 +1,142 @@ +import 'package:flutter/material.dart'; +import 'package:stream_chat_flutter/src/stream_icons.dart'; + +typedef ChipBuilder = Widget Function(BuildContext context, T chip); +typedef OnChipAdded = void Function(T chip); +typedef OnChipRemoved = void Function(T chip); + +class ChipsInputTextField extends StatefulWidget { + final TextEditingController controller; + final FocusNode focusNode; + final ValueChanged onInputChanged; + final ChipBuilder chipBuilder; + final OnChipAdded onChipAdded; + final OnChipRemoved onChipRemoved; + final String hint; + + const ChipsInputTextField({ + Key key, + @required this.chipBuilder, + @required this.controller, + this.onInputChanged, + this.focusNode, + this.onChipAdded, + this.onChipRemoved, + this.hint = 'Type a name or group', + }) : super(key: key); + + @override + ChipInputTextFieldState createState() => ChipInputTextFieldState(); +} + +class ChipInputTextFieldState extends State> { + final _chips = {}; + bool _pauseItemAddition = false; + + void addItem(T item) { + if (!_pauseItemAddition) { + setState(() => _chips.add(item)); + if (widget.onChipAdded != null) widget.onChipAdded(item); + } + } + + void removeItem(T item) { + setState(() { + _chips.remove(item); + if (_chips.isEmpty) resumeItemAddition(); + }); + if (widget.focusNode != null) widget.focusNode.requestFocus(); + if (widget.onChipRemoved != null) widget.onChipRemoved(item); + } + + void pauseItemAddition() { + if (!_pauseItemAddition) { + setState(() => _pauseItemAddition = true); + } + } + + void resumeItemAddition() { + if (_pauseItemAddition) { + setState(() => _pauseItemAddition = false); + } + } + + @override + Widget build(BuildContext context) { + return Material( + elevation: 4, + color: Colors.white, + child: Container( + child: Padding( + padding: const EdgeInsets.symmetric( + vertical: 16.0, + horizontal: 16.0, + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.symmetric(vertical: 10.0), + child: Text( + 'TO:', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500, + ), + ), + ), + SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Wrap( + spacing: 4.0, + runSpacing: 4.0, + children: _chips.map((item) { + return widget.chipBuilder(context, item); + }).toList(), + ), + if (!_pauseItemAddition) ...[ + if (_chips.isNotEmpty) SizedBox(height: 4), + TextField( + controller: widget.controller, + onChanged: widget.onInputChanged, + focusNode: widget.focusNode, + style: TextStyle(fontSize: 18), + decoration: InputDecoration( + isDense: true, + border: InputBorder.none, + focusedBorder: InputBorder.none, + enabledBorder: InputBorder.none, + errorBorder: InputBorder.none, + disabledBorder: InputBorder.none, + contentPadding: const EdgeInsets.symmetric( + vertical: 8, + ), + hintText: widget.hint, + hintStyle: TextStyle(fontSize: 18)), + ), + ] + ], + ), + ), + SizedBox(width: 12), + IconButton( + icon: Icon( + _chips.isEmpty ? StreamIcons.user : StreamIcons.user_add, + ), + onPressed: () { + resumeItemAddition(); + }, + visualDensity: VisualDensity.compact, + padding: const EdgeInsets.all(0), + ), + ], + ), + ), + ), + ); + } +} diff --git a/example/lib/main.dart b/example/lib/main.dart index 0f84c3a8..fa1c14a6 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -6,6 +6,7 @@ import 'package:flutter/foundation.dart'; 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 'notifications_service.dart'; @@ -268,7 +269,7 @@ class CreateChannelPage extends StatefulWidget { } class _CreateChannelPageState extends State { - List selectedUsers = []; + final selectedUsers = {}; @override Widget build(BuildContext context) { @@ -282,7 +283,7 @@ class _CreateChannelPageState extends State { ), ), floatingActionButton: - selectedUsers.isNotEmpty ? _buildFAB(context) : SizedBox(), + selectedUsers.isNotEmpty ? _buildFAB(context) : null, body: _buildListView(), ); } @@ -325,7 +326,7 @@ class _CreateChannelPageState extends State { } } - _createChannel(context, selectedUsers, name); + _createChannel(context, selectedUsers.toList(), name); }, ); } @@ -407,10 +408,44 @@ class NewChatScreen extends StatefulWidget { } class _NewChatScreenState extends State { + final _chipInputTextFieldStateKey = + GlobalKey>(); + + TextEditingController _controller; + + ChipInputTextFieldState get _chipInputTextFieldState => + _chipInputTextFieldStateKey.currentState; + + String _userNameQuery = ''; + + final _selectedUsers = {}; + + bool _isSearchActive = false; + + @override + void initState() { + super.initState(); + _controller = TextEditingController() + ..addListener(() { + setState(() { + _userNameQuery = _controller.text; + _isSearchActive = _userNameQuery.isNotEmpty; + }); + }); + } + + @override + void dispose() { + _controller?.clear(); + _controller?.dispose(); + super.dispose(); + } + @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( + elevation: 0, backgroundColor: Colors.white, title: Text( 'New Chat', @@ -418,14 +453,71 @@ class _NewChatScreenState extends State { ), ), body: UsersBloc( - child: UserListView( - pagination: PaginationParams( - limit: 25, - ), - sort: [ - SortOption( - 'name', - direction: SortOption.ASC, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ChipsInputTextField( + key: _chipInputTextFieldStateKey, + controller: _controller, + focusNode: FocusNode(), + chipBuilder: (context, user) { + return InputChip( + key: ObjectKey(user), + label: Text( + user.name, + style: TextStyle(color: Colors.black), + ), + avatar: UserAvatar( + user: user, + ), + onDeleted: () => _chipInputTextFieldState.removeItem(user), + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + ); + }, + onChipAdded: (user) { + setState(() => _selectedUsers.add(user)); + }, + onChipRemoved: (user) { + setState(() => _selectedUsers.remove(user)); + }, + ), + Container( + width: double.maxFinite, + color: Colors.white54, + child: Padding( + padding: const EdgeInsets.symmetric( + vertical: 8, + horizontal: 8, + ), + child: Text( + _isSearchActive + ? "Matches for \"$_userNameQuery\"" + : 'On the platform', + style: TextStyle( + fontWeight: FontWeight.w500, + ), + ), + ), + ), + Expanded( + child: UserListView( + filterByUserName: _userNameQuery, + selectedUsers: _selectedUsers, + groupAlphabetically: _isSearchActive ? false : true, + onUserTap: (user, _) { + if (!_selectedUsers.contains(user)) { + _controller.clear(); + _chipInputTextFieldState + ..addItem(user) + ..pauseItemAddition(); + } + }, + pagination: PaginationParams( + limit: 25, + ), + ), + ), + MessageInput( ), ], ),