diff --git a/lib/helpers/conversations_helper.dart b/lib/helpers/conversations_helper.dart index 34828e1..d50cdad 100644 --- a/lib/helpers/conversations_helper.dart +++ b/lib/helpers/conversations_helper.dart @@ -8,6 +8,7 @@ import 'package:comunic/models/api_request.dart'; import 'package:comunic/models/api_response.dart'; import 'package:comunic/models/conversation.dart'; import 'package:comunic/models/conversation_message.dart'; +import 'package:comunic/models/conversation_settings.dart'; import 'package:comunic/models/new_conversation_message.dart'; import 'package:comunic/utils/account_utils.dart'; import 'package:meta/meta.dart'; @@ -24,6 +25,27 @@ class ConversationsHelper { final ConversationMessagesDatabaseHelper _conversationMessagesDatabaseHelper = ConversationMessagesDatabaseHelper(); + /// Create a new conversation + /// + /// Return the ID of the newly created conversation or -1 in case of failure + Future createConversation(ConversationSettings settings) async { + + final response = await APIRequest( + uri: "conversations/create", + needLogin: true, + args: { + "name" : settings.hasName ? settings.name : "false", + "follow" : settings.following ? "true" : "false", + "users": settings.members.join(",") + } + ).exec(); + + if(response.code != 200) return -1; + + return response.getObject()["conversationID"]; + + } + /// Download the list of conversations from the server Future downloadList() async { final response = diff --git a/lib/models/conversation_settings.dart b/lib/models/conversation_settings.dart new file mode 100644 index 0000000..786d5a5 --- /dev/null +++ b/lib/models/conversation_settings.dart @@ -0,0 +1,27 @@ +import 'package:meta/meta.dart'; + +/// Conversation settings model +/// +/// Use this model to create / update a conversation +/// +/// @author Pierre HUBERT + +class ConversationSettings { + /// Set the ID to 0 if not required + final int id; + final String name; + final bool following; + final List members; + + ConversationSettings({ + @required this.id, + @required this.name, + @required this.following, + @required this.members, + }) : assert(members != null && members.length > 0), + assert(following != null); + + bool get hasName => name != null && name.length > 0; + + bool get hasId => id != null & id > 0; +} diff --git a/lib/ui/routes/create_conversation_route.dart b/lib/ui/routes/create_conversation_route.dart new file mode 100644 index 0000000..c0a2f65 --- /dev/null +++ b/lib/ui/routes/create_conversation_route.dart @@ -0,0 +1,20 @@ +import 'package:comunic/ui/screens/update_conversation_screen.dart'; +import 'package:comunic/utils/intl_utils.dart'; +import 'package:flutter/material.dart'; + +/// Create a new conversation route +/// +/// @author Pierre HUBERT + +class CreateConversationRoute extends StatelessWidget { + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text(tr("Create a conversation")), + ), + + body: UpdateConversationScreen(), + ); + } +} diff --git a/lib/ui/screens/conversations_list_screen.dart b/lib/ui/screens/conversations_list_screen.dart index 7400e26..8df68db 100644 --- a/lib/ui/screens/conversations_list_screen.dart +++ b/lib/ui/screens/conversations_list_screen.dart @@ -3,6 +3,7 @@ import 'package:comunic/helpers/conversations_helper.dart'; import 'package:comunic/helpers/users_helper.dart'; import 'package:comunic/lists/conversations_list.dart'; import 'package:comunic/ui/routes/conversation_route.dart'; +import 'package:comunic/ui/routes/create_conversation_route.dart'; import 'package:comunic/ui/tiles/conversation_tile.dart'; import 'package:comunic/utils/intl_utils.dart'; import 'package:comunic/utils/ui_utils.dart'; @@ -52,7 +53,7 @@ class _ConversationScreenState extends State { //Get the list of conversations var list; - if(cached) + if (cached) list = await _conversationsHelper.getCachedList(); else list = await _conversationsHelper.downloadList(); @@ -90,12 +91,20 @@ class _ConversationScreenState extends State { } /// Open a conversation - void _openConversation(BuildContext context, int conversationId){ - Navigator.of(context).push(MaterialPageRoute(builder: (c){ - return ConversationRoute(conversationID: conversationId,); + void _openConversation(BuildContext context, int conversationId) { + Navigator.of(context).push(MaterialPageRoute(builder: (c) { + return ConversationRoute( + conversationID: conversationId, + ); })); } + /// Create a new conversation + void _createConversation(BuildContext context) { + Navigator.of(context) + .push(MaterialPageRoute(builder: (c) => CreateConversationRoute())); + } + @override Widget build(BuildContext context) { if (_error == LoadErrorLevel.MAJOR) return _buildErrorCard(); @@ -116,7 +125,9 @@ class _ConversationScreenState extends State { return ConversationTile( conversation: _list.elementAt(index), usersList: _list.users, - onOpen: (c){_openConversation(context, c.id);}, + onOpen: (c) { + _openConversation(context, c.id); + }, ); }, itemCount: _list.length, @@ -124,6 +135,18 @@ class _ConversationScreenState extends State { ), ], ), + + // Add conversation button + Positioned( + right: 20.0, + bottom: 20.0, + child: FloatingActionButton( + onPressed: () => _createConversation(context), + child: Icon(Icons.add), + ), + ), + + // Loading indicator Positioned( top: 8.0, left: 0.0, diff --git a/lib/ui/screens/update_conversation_screen.dart b/lib/ui/screens/update_conversation_screen.dart new file mode 100644 index 0000000..2509cb0 --- /dev/null +++ b/lib/ui/screens/update_conversation_screen.dart @@ -0,0 +1,132 @@ +import 'package:comunic/helpers/conversations_helper.dart'; +import 'package:comunic/lists/users_list.dart'; +import 'package:comunic/models/conversation_settings.dart'; +import 'package:comunic/ui/routes/conversation_route.dart'; +import 'package:comunic/ui/tiles/simple_user_tile.dart'; +import 'package:comunic/ui/widgets/pick_user_widget.dart'; +import 'package:comunic/utils/intl_utils.dart'; +import 'package:flutter/material.dart'; + +/// Create / Update conversation screen +/// +/// @author Pierre HUBERT + +enum _MembersMenuChoices { REMOVE } + +class UpdateConversationScreen extends StatefulWidget { + @override + State createState() => _UpdateConversationScreen(); +} + +class _UpdateConversationScreen extends State { + TextEditingController _nameController = TextEditingController(); + UsersList _members = UsersList(); + bool _followConversation = true; + + @override + Widget build(BuildContext context) { + return Container( + padding: EdgeInsets.all(8.0), + child: Column( + children: [ + // Conversation name + TextField( + controller: _nameController, + decoration: InputDecoration( + labelText: tr("Conversation name (optionnal)"), + alignLabelWithHint: true), + ), + + // Add a member to the conversation + PickUserWidget( + resetOnChoose: true, + keepFocusOnChoose: true, + label: tr("Add member"), + onSelectUser: (user) => setState(() { + if (!_members.contains(user)) _members.add(user); + }), + ), + + //Conversation members + Container( + child: _members.length == 0 + ? null + : Flexible( + child: ListView.builder( + itemCount: _members.length, + itemBuilder: (b, i) { + return SimpleUserTile( + user: _members[i], + trailing: PopupMenuButton<_MembersMenuChoices>( + onSelected: (choice) => + _membersMenuItemSelected(i, choice), + itemBuilder: (c) => + >[ + PopupMenuItem( + child: Text(tr("Remove")), + value: _MembersMenuChoices.REMOVE, + ) + ], + ), + ); + }, + ), + ), + ), + + // Follow conversation ? + Row( + children: [ + Switch( + value: _followConversation, + onChanged: (b) => setState(() { + _followConversation = b; + }), + ), + Text(tr("Follow conversation")) + ], + ), + + // Submit button + RaisedButton( + onPressed: _members.length < 1 ? null : _submitForm, + child: Text(tr("Create the conversation")), + ) + ], + ), + ); + } + + /// An option of the members menu has been selected + void _membersMenuItemSelected(int index, _MembersMenuChoices choice) { + if (choice == null) return; + + if (choice == _MembersMenuChoices.REMOVE) + return setState(() { + _members.removeAt(index); + }); + } + + /// Submit the conversation + Future _submitForm() async { + final settings = ConversationSettings( + id: 0, + name: _nameController.text, + following: _followConversation, + members: _members.usersID, + ); + + // Create the conversation + final conversationID = await ConversationsHelper().createConversation(settings); + + // Check for errors + if(conversationID < 1) + return Scaffold.of(context).showSnackBar( + SnackBar(content: Text(tr("Could not create the conversation!")), duration: Duration(seconds: 1),)); + + // Open the conversation + Navigator.of(context).pushReplacement(MaterialPageRoute( + builder: (c) => ConversationRoute(conversationID: conversationID,) + )); + } +} diff --git a/lib/ui/tiles/simple_user_tile.dart b/lib/ui/tiles/simple_user_tile.dart index e9fd87d..026fbd3 100644 --- a/lib/ui/tiles/simple_user_tile.dart +++ b/lib/ui/tiles/simple_user_tile.dart @@ -13,8 +13,9 @@ typedef OnUserTap = void Function(User); class SimpleUserTile extends StatelessWidget { final User user; final OnUserTap onTap; + final Widget trailing; - const SimpleUserTile({Key key, this.user, this.onTap}) + const SimpleUserTile({Key key, this.user, this.onTap, this.trailing}) : assert(user != null), super(key: key); @@ -26,6 +27,7 @@ class SimpleUserTile extends StatelessWidget { user: user, ), title: Text(user.fullName), + trailing: trailing, ); } } diff --git a/lib/ui/widgets/pick_user_widget.dart b/lib/ui/widgets/pick_user_widget.dart index b633f13..4014e1a 100644 --- a/lib/ui/widgets/pick_user_widget.dart +++ b/lib/ui/widgets/pick_user_widget.dart @@ -2,7 +2,6 @@ import 'package:comunic/helpers/search_helper.dart'; import 'package:comunic/lists/users_list.dart'; import 'package:comunic/models/user.dart'; import 'package:comunic/ui/tiles/simple_user_tile.dart'; -import 'package:comunic/utils/intl_utils.dart'; import 'package:flutter/material.dart'; /// Pick user widget @@ -16,9 +15,20 @@ typedef OnSelectUserCallback = void Function(User); class PickUserWidget extends StatefulWidget { final OnSelectUserCallback onSelectUser; + final String label; + final bool resetOnChoose; + final bool keepFocusOnChoose; - const PickUserWidget({Key key, @required this.onSelectUser}) + const PickUserWidget( + {Key key, + @required this.onSelectUser, + @required this.label, + this.resetOnChoose = false, + this.keepFocusOnChoose = false}) : assert(onSelectUser != null), + assert(label != null), + assert(resetOnChoose != null), + assert(keepFocusOnChoose != null), super(key: key); @override @@ -42,8 +52,7 @@ class _PickUserWidgetState extends State { _focusNode.addListener(() { if (_focusNode.hasFocus) { //Check for focus - _overlayEntry = _createOverlayEntry(); - Overlay.of(context).insert(_overlayEntry); + //_showOverlay(); } else { //Remove overlay _removeOverlay(); @@ -57,7 +66,10 @@ class _PickUserWidgetState extends State { focusNode: _focusNode, onChanged: (s) => _updateSuggestions(), controller: _controller, - decoration: InputDecoration(labelText: tr("Select user")), + decoration: InputDecoration( + labelText: widget.label, + alignLabelWithHint: true, + ), ); } @@ -87,6 +99,11 @@ class _PickUserWidgetState extends State { }); } + void _showOverlay() { + _overlayEntry = _createOverlayEntry(); + Overlay.of(context).insert(_overlayEntry); + } + void _removeOverlay() { if (_overlayEntry != null) { _overlayEntry.remove(); @@ -96,22 +113,36 @@ class _PickUserWidgetState extends State { /// This method get called each time the input value is updated Future _updateSuggestions() async { - if (_controller.value.text.length == 0) return; + if (_controller.value.text.length == 0) return _removeOverlay(); final results = await _searchHelper.searchUser(_controller.value.text); if (results == null) return; _suggestions = results; - if (_overlayEntry != null) _overlayEntry.markNeedsBuild(); + if (_overlayEntry != null) + _overlayEntry.markNeedsBuild(); + else + _showOverlay(); } /// Method called each time a user is tapped (selected) void _userTapped(User user) { - _controller.text = user.fullName; + // Hide overlay _removeOverlay(); - _focusNode.unfocus(); + // Unfocus if required + if (!widget.keepFocusOnChoose) { + _focusNode.unfocus(); + } + + //Check if name has to remain in input + if (widget.resetOnChoose) { + _controller.text = ""; + } else + _controller.text = user.fullName; + + //Callback widget.onSelectUser(user); } }