1
0
mirror of https://gitlab.com/comunic/comunicmobile synced 2024-11-22 21:09:21 +00:00

Create new conversations

This commit is contained in:
Pierre HUBERT 2019-04-27 16:23:08 +02:00
parent 765ca82300
commit 852064e82d
7 changed files with 272 additions and 15 deletions

View File

@ -8,6 +8,7 @@ import 'package:comunic/models/api_request.dart';
import 'package:comunic/models/api_response.dart'; import 'package:comunic/models/api_response.dart';
import 'package:comunic/models/conversation.dart'; import 'package:comunic/models/conversation.dart';
import 'package:comunic/models/conversation_message.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/models/new_conversation_message.dart';
import 'package:comunic/utils/account_utils.dart'; import 'package:comunic/utils/account_utils.dart';
import 'package:meta/meta.dart'; import 'package:meta/meta.dart';
@ -24,6 +25,27 @@ class ConversationsHelper {
final ConversationMessagesDatabaseHelper _conversationMessagesDatabaseHelper = final ConversationMessagesDatabaseHelper _conversationMessagesDatabaseHelper =
ConversationMessagesDatabaseHelper(); ConversationMessagesDatabaseHelper();
/// Create a new conversation
///
/// Return the ID of the newly created conversation or -1 in case of failure
Future<int> 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 /// Download the list of conversations from the server
Future<ConversationsList> downloadList() async { Future<ConversationsList> downloadList() async {
final response = final response =

View File

@ -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<int> 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;
}

View File

@ -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(),
);
}
}

View File

@ -3,6 +3,7 @@ import 'package:comunic/helpers/conversations_helper.dart';
import 'package:comunic/helpers/users_helper.dart'; import 'package:comunic/helpers/users_helper.dart';
import 'package:comunic/lists/conversations_list.dart'; import 'package:comunic/lists/conversations_list.dart';
import 'package:comunic/ui/routes/conversation_route.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/ui/tiles/conversation_tile.dart';
import 'package:comunic/utils/intl_utils.dart'; import 'package:comunic/utils/intl_utils.dart';
import 'package:comunic/utils/ui_utils.dart'; import 'package:comunic/utils/ui_utils.dart';
@ -52,7 +53,7 @@ class _ConversationScreenState extends State<ConversationsListScreen> {
//Get the list of conversations //Get the list of conversations
var list; var list;
if(cached) if (cached)
list = await _conversationsHelper.getCachedList(); list = await _conversationsHelper.getCachedList();
else else
list = await _conversationsHelper.downloadList(); list = await _conversationsHelper.downloadList();
@ -90,12 +91,20 @@ class _ConversationScreenState extends State<ConversationsListScreen> {
} }
/// Open a conversation /// Open a conversation
void _openConversation(BuildContext context, int conversationId){ void _openConversation(BuildContext context, int conversationId) {
Navigator.of(context).push(MaterialPageRoute(builder: (c){ Navigator.of(context).push(MaterialPageRoute(builder: (c) {
return ConversationRoute(conversationID: conversationId,); return ConversationRoute(
conversationID: conversationId,
);
})); }));
} }
/// Create a new conversation
void _createConversation(BuildContext context) {
Navigator.of(context)
.push(MaterialPageRoute(builder: (c) => CreateConversationRoute()));
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (_error == LoadErrorLevel.MAJOR) return _buildErrorCard(); if (_error == LoadErrorLevel.MAJOR) return _buildErrorCard();
@ -116,7 +125,9 @@ class _ConversationScreenState extends State<ConversationsListScreen> {
return ConversationTile( return ConversationTile(
conversation: _list.elementAt(index), conversation: _list.elementAt(index),
usersList: _list.users, usersList: _list.users,
onOpen: (c){_openConversation(context, c.id);}, onOpen: (c) {
_openConversation(context, c.id);
},
); );
}, },
itemCount: _list.length, itemCount: _list.length,
@ -124,6 +135,18 @@ class _ConversationScreenState extends State<ConversationsListScreen> {
), ),
], ],
), ),
// Add conversation button
Positioned(
right: 20.0,
bottom: 20.0,
child: FloatingActionButton(
onPressed: () => _createConversation(context),
child: Icon(Icons.add),
),
),
// Loading indicator
Positioned( Positioned(
top: 8.0, top: 8.0,
left: 0.0, left: 0.0,

View File

@ -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<StatefulWidget> createState() => _UpdateConversationScreen();
}
class _UpdateConversationScreen extends State<UpdateConversationScreen> {
TextEditingController _nameController = TextEditingController();
UsersList _members = UsersList();
bool _followConversation = true;
@override
Widget build(BuildContext context) {
return Container(
padding: EdgeInsets.all(8.0),
child: Column(
children: <Widget>[
// 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) =>
<PopupMenuEntry<_MembersMenuChoices>>[
PopupMenuItem(
child: Text(tr("Remove")),
value: _MembersMenuChoices.REMOVE,
)
],
),
);
},
),
),
),
// Follow conversation ?
Row(
children: <Widget>[
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<void> _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,)
));
}
}

View File

@ -13,8 +13,9 @@ typedef OnUserTap = void Function(User);
class SimpleUserTile extends StatelessWidget { class SimpleUserTile extends StatelessWidget {
final User user; final User user;
final OnUserTap onTap; 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), : assert(user != null),
super(key: key); super(key: key);
@ -26,6 +27,7 @@ class SimpleUserTile extends StatelessWidget {
user: user, user: user,
), ),
title: Text(user.fullName), title: Text(user.fullName),
trailing: trailing,
); );
} }
} }

View File

@ -2,7 +2,6 @@ import 'package:comunic/helpers/search_helper.dart';
import 'package:comunic/lists/users_list.dart'; import 'package:comunic/lists/users_list.dart';
import 'package:comunic/models/user.dart'; import 'package:comunic/models/user.dart';
import 'package:comunic/ui/tiles/simple_user_tile.dart'; import 'package:comunic/ui/tiles/simple_user_tile.dart';
import 'package:comunic/utils/intl_utils.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
/// Pick user widget /// Pick user widget
@ -16,9 +15,20 @@ typedef OnSelectUserCallback = void Function(User);
class PickUserWidget extends StatefulWidget { class PickUserWidget extends StatefulWidget {
final OnSelectUserCallback onSelectUser; 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(onSelectUser != null),
assert(label != null),
assert(resetOnChoose != null),
assert(keepFocusOnChoose != null),
super(key: key); super(key: key);
@override @override
@ -42,8 +52,7 @@ class _PickUserWidgetState extends State<PickUserWidget> {
_focusNode.addListener(() { _focusNode.addListener(() {
if (_focusNode.hasFocus) { if (_focusNode.hasFocus) {
//Check for focus //Check for focus
_overlayEntry = _createOverlayEntry(); //_showOverlay();
Overlay.of(context).insert(_overlayEntry);
} else { } else {
//Remove overlay //Remove overlay
_removeOverlay(); _removeOverlay();
@ -57,7 +66,10 @@ class _PickUserWidgetState extends State<PickUserWidget> {
focusNode: _focusNode, focusNode: _focusNode,
onChanged: (s) => _updateSuggestions(), onChanged: (s) => _updateSuggestions(),
controller: _controller, controller: _controller,
decoration: InputDecoration(labelText: tr("Select user")), decoration: InputDecoration(
labelText: widget.label,
alignLabelWithHint: true,
),
); );
} }
@ -87,6 +99,11 @@ class _PickUserWidgetState extends State<PickUserWidget> {
}); });
} }
void _showOverlay() {
_overlayEntry = _createOverlayEntry();
Overlay.of(context).insert(_overlayEntry);
}
void _removeOverlay() { void _removeOverlay() {
if (_overlayEntry != null) { if (_overlayEntry != null) {
_overlayEntry.remove(); _overlayEntry.remove();
@ -96,22 +113,36 @@ class _PickUserWidgetState extends State<PickUserWidget> {
/// This method get called each time the input value is updated /// This method get called each time the input value is updated
Future<void> _updateSuggestions() async { Future<void> _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); final results = await _searchHelper.searchUser(_controller.value.text);
if (results == null) return; if (results == null) return;
_suggestions = results; _suggestions = results;
if (_overlayEntry != null) _overlayEntry.markNeedsBuild(); if (_overlayEntry != null)
_overlayEntry.markNeedsBuild();
else
_showOverlay();
} }
/// Method called each time a user is tapped (selected) /// Method called each time a user is tapped (selected)
void _userTapped(User user) { void _userTapped(User user) {
_controller.text = user.fullName; // Hide overlay
_removeOverlay(); _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); widget.onSelectUser(user);
} }
} }