From 682c0b3e10736746b8d7f73cc38b93e03fb87946 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Fri, 20 Nov 2020 18:03:33 +0100 Subject: [PATCH] swap user list with message list and create channel with draft: true --- example/lib/chips_input_text_field.dart | 157 +++++----- example/lib/main.dart | 382 ++++++++++++++---------- example/pubspec.yaml | 2 +- lib/src/message_input.dart | 8 +- lib/src/message_list_view.dart | 19 ++ lib/src/url_attachment.dart | 6 +- 6 files changed, 339 insertions(+), 235 deletions(-) diff --git a/example/lib/chips_input_text_field.dart b/example/lib/chips_input_text_field.dart index 30183926..317a68f7 100644 --- a/example/lib/chips_input_text_field.dart +++ b/example/lib/chips_input_text_field.dart @@ -22,7 +22,7 @@ class ChipsInputTextField extends StatefulWidget { this.focusNode, this.onChipAdded, this.onChipRemoved, - this.hint = 'Type a name or group', + this.hint = 'Type a name', }) : super(key: key); @override @@ -61,83 +61,96 @@ class ChipInputTextFieldState extends State> { @override Widget build(BuildContext context) { - return Material( - elevation: 1, - color: Colors.white, - child: Container( - child: Padding( - padding: const EdgeInsets.fromLTRB(16, 16, 16, 16), - child: IntrinsicHeight( - child: Row( - crossAxisAlignment: CrossAxisAlignment.baseline, - children: [ - Padding( - padding: const EdgeInsets.symmetric(vertical: 4.0), - child: Text( - 'TO:', - style: TextStyle( - fontSize: 12, - color: Colors.black.withOpacity(0.5), + return GestureDetector( + onTap: _pauseItemAddition + ? () { + setState(() { + _pauseItemAddition = false; + widget.focusNode?.requestFocus(); + }); + } + : null, + child: Material( + elevation: 1, + color: Colors.white, + child: Container( + child: Padding( + padding: const EdgeInsets.fromLTRB(16, 16, 16, 16), + child: IntrinsicHeight( + child: Row( + crossAxisAlignment: CrossAxisAlignment.baseline, + children: [ + Padding( + padding: const EdgeInsets.symmetric(vertical: 4.0), + child: Text( + 'TO:', + style: TextStyle( + fontSize: 12, + color: Colors.black.withOpacity(0.5), + ), ), ), - ), - SizedBox(width: 12), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - Wrap( - spacing: 8.0, - runSpacing: 4.0, - children: _chips.map((item) { - return widget.chipBuilder(context, item); - }).toList(), - ), - if (!_pauseItemAddition) - TextField( - controller: widget.controller, - onChanged: widget.onInputChanged, - focusNode: widget.focusNode, - decoration: InputDecoration( - isDense: true, - border: InputBorder.none, - focusedBorder: InputBorder.none, - enabledBorder: InputBorder.none, - errorBorder: InputBorder.none, - disabledBorder: InputBorder.none, - contentPadding: const EdgeInsets.only(top: 4.0), - hintText: widget.hint, - hintStyle: TextStyle( - color: Colors.black.withOpacity(0.5), - fontSize: 14, + SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Wrap( + spacing: 8.0, + runSpacing: 4.0, + children: _chips.map((item) { + return widget.chipBuilder(context, item); + }).toList(), + ), + if (!_pauseItemAddition) + TextField( + controller: widget.controller, + onChanged: widget.onInputChanged, + focusNode: widget.focusNode, + decoration: InputDecoration( + isDense: true, + border: InputBorder.none, + focusedBorder: InputBorder.none, + enabledBorder: InputBorder.none, + errorBorder: InputBorder.none, + disabledBorder: InputBorder.none, + contentPadding: const EdgeInsets.only(top: 4.0), + hintText: widget.hint, + hintStyle: TextStyle( + color: Colors.black.withOpacity(0.5), + fontSize: 14, + ), ), ), - ), - ], - ), - ), - SizedBox(width: 12), - Align( - alignment: Alignment.bottomCenter, - child: IconButton( - icon: Icon( - _chips.isEmpty ? StreamIcons.user : StreamIcons.user_add, - color: Colors.black.withOpacity(0.5), - size: 24, - ), - onPressed: !_pauseItemAddition ? null : resumeItemAddition, - alignment: Alignment.topRight, - visualDensity: VisualDensity.compact, - padding: const EdgeInsets.all(0), - splashRadius: 24, - constraints: BoxConstraints.tightFor( - height: 24, - width: 24, + ], ), ), - ), - ], + SizedBox(width: 12), + Align( + alignment: Alignment.bottomCenter, + child: IconButton( + icon: Icon( + _chips.isEmpty + ? StreamIcons.user + : StreamIcons.user_add, + color: Colors.black.withOpacity(0.5), + size: 24, + ), + onPressed: + !_pauseItemAddition ? null : resumeItemAddition, + alignment: Alignment.topRight, + visualDensity: VisualDensity.compact, + padding: const EdgeInsets.all(0), + splashRadius: 24, + constraints: BoxConstraints.tightFor( + height: 24, + width: 24, + ), + ), + ), + ], + ), ), ), ), diff --git a/example/lib/main.dart b/example/lib/main.dart index 70c11940..e2a8a5f7 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -111,6 +111,7 @@ class ChannelListPage extends StatelessWidget { ), ListTile( onTap: () { + Navigator.pop(context); Navigator.push( context, MaterialPageRoute(builder: (_) => NewChatScreen()), @@ -126,6 +127,7 @@ class ChannelListPage extends StatelessWidget { ), ListTile( onTap: () { + Navigator.pop(context); Navigator.push( context, MaterialPageRoute(builder: (_) => NewGroupChatScreen()), @@ -177,7 +179,10 @@ class ChannelListPage extends StatelessWidget { filter: { 'members': { '\$in': [user.id], - } + }, + 'draft': { + r'$ne': true, + }, }, options: { 'presence': true, @@ -283,12 +288,17 @@ class _NewChatScreenState extends State { 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), () { @@ -305,10 +315,50 @@ class _NewChatScreenState extends State { 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(); @@ -335,81 +385,81 @@ class _NewChatScreenState extends State { body: StreamChannel( showLoading: false, channel: channel, - child: UsersBloc( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - ChipsInputTextField( - key: _chipInputTextFieldStateKey, - controller: _controller, - focusNode: FocusNode(), - 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), - ), + 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, + ), + 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), + ), ), - ), - ], - ); - }, - 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, ), - SizedBox(width: 8), - Text( - 'Create a Group', - style: TextStyle( - fontWeight: FontWeight.bold, - fontSize: 16, - ), - ), - ], - ), + ), + ], ), ), ), + ), + if (_showUserList) Container( width: double.maxFinite, decoration: BoxDecoration( @@ -438,97 +488,108 @@ class _NewChatScreenState extends State { ), ), ), - Expanded( - 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, - } - }, - 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, + 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...'), + ], ), ), - Text('No user matches these keywords...'), - ], - ), - ), - ), - ); - }, - ); - }, - ), - ), - MessageInput( - preMessageSending: (message) async { - channel.extraData = { - 'members': [ - ..._selectedUsers.map((e) => e.id), - channel.client.state.user.id, - ], - }; - await channel.watch(); - return message; - }, - onMessageSent: (_) { - Navigator.pushReplacement( - context, - MaterialPageRoute( - builder: (context) { - return StreamChannel( - child: ChannelPage(), - channel: channel, - ); - }, - ), - ); - }, - ), - ], - ), + ), + ); + }, + ); + }, + ), + ) + : 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 { @@ -761,7 +822,10 @@ class _NewGroupChatScreenState extends State { if (_userNameQuery.isNotEmpty) 'name': { r'$autocomplete': _userNameQuery, - } + }, + 'id': { + r'$ne': StreamChat.of(context).user.id, + } }, sort: [ SortOption( @@ -832,9 +896,11 @@ class _GroupChatDetailsScreenState extends State { void _groupNameListener() { final name = _groupNameController.text; - setState(() { - _isGroupNameEmpty = name.isEmpty; - }); + if (mounted) { + setState(() { + _isGroupNameEmpty = name.isEmpty; + }); + } } @override diff --git a/example/pubspec.yaml b/example/pubspec.yaml index e5c11897..0fe6a0ad 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -1,6 +1,6 @@ name: example description: A new Flutter project. -version: 1.0.61+63 +version: 1.0.62+64 environment: sdk: ">=2.2.2 <3.0.0" diff --git a/lib/src/message_input.dart b/lib/src/message_input.dart index 423d4c8f..6845d1a4 100644 --- a/lib/src/message_input.dart +++ b/lib/src/message_input.dart @@ -104,6 +104,7 @@ class MessageInput extends StatefulWidget { this.actions, this.actionsLocation = ActionsLocation.left, this.attachmentThumbnailBuilders, + this.focusNode, }) : super(key: key); /// Message to edit @@ -149,6 +150,9 @@ class MessageInput extends StatefulWidget { /// Map that defines a thumbnail builder for an attachment type final Map attachmentThumbnailBuilders; + /// The focus node associated to the TextField + final FocusNode focusNode; + @override MessageInputState createState() => MessageInputState(); @@ -169,10 +173,10 @@ class MessageInput extends StatefulWidget { class MessageInputState extends State { final List<_SendingAttachment> _attachments = []; - final _focusNode = FocusNode(); final List _mentionedUsers = []; final _imagePicker = ImagePicker(); + FocusNode _focusNode; bool _inputEnabled = true; bool _messageIsPresent = false; bool _animateContainer = true; @@ -1708,6 +1712,8 @@ class MessageInputState extends State { void initState() { super.initState(); + _focusNode = widget.focusNode ?? FocusNode(); + _emojiNames = Emoji.all().map((e) => e.name); if (!kIsWeb) { diff --git a/lib/src/message_list_view.dart b/lib/src/message_list_view.dart index 2a60b75b..4a5e445b 100644 --- a/lib/src/message_list_view.dart +++ b/lib/src/message_list_view.dart @@ -180,7 +180,26 @@ class _MessageListViewState extends State { ? streamChannel.channel.state.threads[widget.parentMessage.id] : streamChannel.channel.state.messages, builder: (context, snapshot) { + if (!snapshot.hasData) { + return Center( + child: CircularProgressIndicator(), + ); + } + final messages = snapshot.data?.reversed?.toList() ?? []; + + if (messages.isEmpty) { + return Center( + child: Text( + 'No chats here yet...', + style: TextStyle( + fontSize: 12, + color: Colors.black.withOpacity(.5), + ), + ), + ); + } + return Stack( alignment: Alignment.center, children: [ diff --git a/lib/src/url_attachment.dart b/lib/src/url_attachment.dart index a873217f..d735ad67 100644 --- a/lib/src/url_attachment.dart +++ b/lib/src/url_attachment.dart @@ -4,9 +4,9 @@ import 'package:stream_chat_flutter/src/utils.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; class UrlAttachment extends StatelessWidget { - Attachment urlAttachment; - String hostDisplayName; - EdgeInsets textPadding; + final Attachment urlAttachment; + final String hostDisplayName; + final EdgeInsets textPadding; UrlAttachment({ @required this.urlAttachment,