diff --git a/lib/helpers/conversations_helper.dart b/lib/helpers/conversations_helper.dart index ad28c08..35b5f1e 100644 --- a/lib/helpers/conversations_helper.dart +++ b/lib/helpers/conversations_helper.dart @@ -292,6 +292,13 @@ class ConversationsHelper { lastMessageID: lastMessageID); } + /// Get a single conversation message from the local database + /// + /// Returns the message if found or null in case of failure + Future getSingleMessageFromCache(int messageID) async { + return await _conversationMessagesDatabaseHelper.get(messageID); + } + /// Send a new message to the server Future sendMessage(NewConversationMessage message) async { final request = APIRequest( @@ -320,27 +327,34 @@ class ConversationsHelper { return SendMessageResult.SUCCESS; } + /// Update a message content + Future updateMessage(int id, String newContent) async { + final response = await APIRequest( + uri: "conversations/updateMessage", + needLogin: true, + args: {"messageID": id.toString(), "content": newContent}).exec(); + + if (response.code != 200) return false; + + // Update the message content locally + return await _conversationMessagesDatabaseHelper.updateMessageContent( + id: id, newContent: newContent); + } + /// Delete permanently a message specified by its [id] Future deleteMessage(int id) async { - // Delete the message online final response = await APIRequest( - uri: "conversations/deleteMessage", - needLogin: true, - args: { - "messageID": id.toString() - } - ).exec(); - - if(response.code != 200) return false; + uri: "conversations/deleteMessage", + needLogin: true, + args: {"messageID": id.toString()}).exec(); + if (response.code != 200) return false; // Delete the message locally return await _conversationMessagesDatabaseHelper.delete(id); - } - /// Turn an API response into a ConversationMessage object ConversationMessage _apiToConversationMessage({ @required int conversationID, diff --git a/lib/helpers/database/conversation_messages_database_helper.dart b/lib/helpers/database/conversation_messages_database_helper.dart index 7a30f95..1a81cd6 100644 --- a/lib/helpers/database/conversation_messages_database_helper.dart +++ b/lib/helpers/database/conversation_messages_database_helper.dart @@ -2,6 +2,7 @@ import 'package:comunic/helpers/database/database_contract.dart'; import 'package:comunic/helpers/database/model_database_helper.dart'; import 'package:comunic/lists/conversation_messages_list.dart'; import 'package:comunic/models/conversation_message.dart'; +import 'package:meta/meta.dart'; /// Conversation messages database helper /// @@ -34,4 +35,26 @@ class ConversationMessagesDatabaseHelper finalList.addAll(list); return finalList; } + + /// Update the content of a message + Future updateMessageContent({ + @required int id, + @required String newContent, + }) async { + assert(id != null); + assert(newContent != null); + + final message = await get(id); + + if(message == null) + return false; + + // Update the conversation message using the map + final map = message.toMap(); + map[ConversationsMessagesTableContract.C_MESSAGE] = newContent; + + await insertOrUpdate(ConversationMessage.fromMap(map)); + + return true; // Success + } } diff --git a/lib/ui/screens/conversation_screen.dart b/lib/ui/screens/conversation_screen.dart index baff86f..4580049 100644 --- a/lib/ui/screens/conversation_screen.dart +++ b/lib/ui/screens/conversation_screen.dart @@ -289,6 +289,7 @@ class _ConversationScreenState extends State { userInfo: _usersInfo.getUser(_messages[i].userID), isLastMessage: _isLastMessage(i), isFirstMessage: _isFirstMessage(i), + onRequestMessageUpdate: _updateMessage, onRequestMessageDelete: _deleteMessage, ); }), @@ -389,6 +390,33 @@ class _ConversationScreenState extends State { ); } + /// Request message content update + Future _updateMessage(ConversationMessage message) async { + final newContent = await askUserString( + context: context, + title: tr("Update message"), + message: tr("Please enter new message content:"), + defaultValue: message.message, + hint: tr("New message")); + + if (newContent == null) return; + + if (!await _conversationsHelper.updateMessage(message.id, newContent)) { + showSimpleSnack(context, tr("Could not update message content!")); + return; + } + + // Get the new version of the conversation message + final newMessage = + await _conversationsHelper.getSingleMessageFromCache(message.id); + + setState(() { + final index = _messages.indexOf(message); + _messages.insert(index, newMessage); + _messages.removeAt(index + 1); + }); + } + /// Request message deletion Future _deleteMessage(ConversationMessage message) async { final choice = await showDialog( @@ -417,11 +445,10 @@ class _ConversationScreenState extends State { ), ); - if(choice == null || !choice) - return; + if (choice == null || !choice) return; // Execute the request - if(!await _conversationsHelper.deleteMessage(message.id)) + if (!await _conversationsHelper.deleteMessage(message.id)) showSimpleSnack(context, tr("Could not delete conversation message!")); // Remove the message from the list diff --git a/lib/ui/tiles/conversation_message_tile.dart b/lib/ui/tiles/conversation_message_tile.dart index b58478a..5fe11dc 100644 --- a/lib/ui/tiles/conversation_message_tile.dart +++ b/lib/ui/tiles/conversation_message_tile.dart @@ -12,8 +12,9 @@ import 'package:flutter/material.dart'; /// /// @author Pierre HUBERT -enum _MenuChoices { DELETE } +enum _MenuChoices { DELETE, REQUEST_UPDATE_CONTENT } +typedef OnRequestMessageUpdate = void Function(ConversationMessage); typedef OnRequestMessageDelete = void Function(ConversationMessage); class ConversationMessageTile extends StatelessWidget { @@ -21,6 +22,7 @@ class ConversationMessageTile extends StatelessWidget { final User userInfo; final bool isLastMessage; final bool isFirstMessage; + final OnRequestMessageUpdate onRequestMessageUpdate; final OnRequestMessageDelete onRequestMessageDelete; const ConversationMessageTile({ @@ -29,11 +31,13 @@ class ConversationMessageTile extends StatelessWidget { @required this.userInfo, @required this.isLastMessage, @required this.isFirstMessage, + @required this.onRequestMessageUpdate, @required this.onRequestMessageDelete, }) : assert(message != null), assert(userInfo != null), assert(isLastMessage != null), assert(isFirstMessage != null), + assert(onRequestMessageUpdate != null), assert(onRequestMessageDelete != null), super(key: key); @@ -47,6 +51,14 @@ class ConversationMessageTile extends StatelessWidget { width: 35.0, ), itemBuilder: (c) => [ + // Update message content + PopupMenuItem( + enabled: message.isOwner, + value: _MenuChoices.REQUEST_UPDATE_CONTENT, + child: Text(tr("Update")), + ), + + // Delete the message PopupMenuItem( enabled: message.isOwner, value: _MenuChoices.DELETE, @@ -256,12 +268,14 @@ class ConversationMessageTile extends StatelessWidget { /// Process menu choice void _menuOptionSelected(_MenuChoices value) { + switch (value) { + case _MenuChoices.REQUEST_UPDATE_CONTENT: + onRequestMessageUpdate(message); + break; - switch(value){ case _MenuChoices.DELETE: onRequestMessageDelete(message); break; } - } } diff --git a/lib/utils/ui_utils.dart b/lib/utils/ui_utils.dart index a999780..3c98e05 100644 --- a/lib/utils/ui_utils.dart +++ b/lib/utils/ui_utils.dart @@ -1,7 +1,10 @@ import 'package:cached_network_image/cached_network_image.dart'; +import 'package:comunic/utils/intl_utils.dart'; import 'package:flutter/material.dart'; /// User interface utilities +/// +/// @author Pierre HUBERT /// Build centered progress bar Widget buildCenteredProgressBar() { @@ -52,17 +55,66 @@ Widget buildErrorCard(String message, {List actions}) { /// Show an image with a given [url] in full screen void showImageFullScreen(BuildContext context, String url) { Navigator.of(context).push(MaterialPageRoute(builder: (c) { - // TODO : add better support later return CachedNetworkImage( imageUrl: url, ); - })); } - /// Show simple snack void showSimpleSnack(BuildContext context, String message) { Scaffold.of(context).showSnackBar(SnackBar(content: Text(message))); -} \ No newline at end of file +} + +/// Show an alert dialog to ask the user to enter a string +Future askUserString({ + @required BuildContext context, + @required String title, + @required String message, + @required String defaultValue, + @required String hint, +}) async { + assert(context != null); + assert(title != null); + assert(message != null); + assert(defaultValue != null); + assert(hint != null); + + TextEditingController controller = TextEditingController(text: defaultValue); + + final confirm = await showDialog( + context: context, + builder: (c) => AlertDialog( + title: Text(title), + content: Column( + children: [ + Text(message), + TextField( + controller: controller, + maxLines: null, + maxLength: 200, + keyboardType: TextInputType.text, + decoration: InputDecoration( + labelText: hint, + alignLabelWithHint: true, + ), + ) + ], + ), + actions: [ + FlatButton( + child: Text(tr("Cancel").toUpperCase()), + onPressed: () => Navigator.pop(c, false), + ), + FlatButton( + child: Text(tr("OK")), + onPressed: () => Navigator.pop(c, true), + ), + ], + )); + + if (confirm == null || !confirm) return null; + + return controller.text; +}