From dacccf57b5e4e53dfc43dc012f168770fbcb8a3f Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Wed, 10 Mar 2021 17:54:41 +0100 Subject: [PATCH] Start conversation upgrade --- lib/constants.dart | 6 + lib/helpers/conversations_helper.dart | 284 +++++++++--------- ...conversation_messages_database_helper.dart | 37 --- .../conversations_database_helper.dart | 29 -- lib/helpers/database/database_contract.dart | 24 -- lib/helpers/database/database_helper.dart | 31 -- .../base_serialization_helper.dart | 121 ++++++++ ...ersation_message_serialization_helper.dart | 50 +++ .../conversations_serialization_helper.dart | 32 ++ lib/helpers/users_helper.dart | 9 +- lib/lists/conversations_list.dart | 11 +- lib/lists/memberships_list.dart | 2 +- lib/lists/unread_conversations_list.dart | 6 +- lib/models/conversation.dart | 104 ++++--- lib/models/conversation_member.dart | 40 +++ lib/models/conversation_message.dart | 189 +++++++++--- lib/models/membership.dart | 2 +- lib/models/new_conversation.dart | 24 ++ lib/models/new_conversation_settings.dart | 27 ++ lib/models/unread_conversation.dart | 19 +- lib/ui/routes/conversation_route.dart | 26 +- lib/ui/routes/update_conversation_route.dart | 21 +- lib/ui/screens/call_screen.dart | 2 +- .../screens/conversation_members_screen.dart | 9 +- lib/ui/screens/conversation_screen.dart | 88 +++--- lib/ui/screens/conversations_list_screen.dart | 38 ++- .../screens/unread_conversations_screen.dart | 12 +- .../screens/update_conversation_screen.dart | 17 +- lib/ui/tiles/conversation_message_tile.dart | 7 +- lib/ui/tiles/conversation_tile.dart | 2 +- lib/ui/widgets/posts_list_widget.dart | 45 +-- .../conversations/conversation_window.dart | 2 +- lib/utils/conversations_utils.dart | 16 +- pubspec.lock | 2 +- pubspec.yaml | 4 +- 35 files changed, 818 insertions(+), 520 deletions(-) create mode 100644 lib/constants.dart delete mode 100644 lib/helpers/database/conversation_messages_database_helper.dart delete mode 100644 lib/helpers/database/conversations_database_helper.dart create mode 100644 lib/helpers/serialization/base_serialization_helper.dart create mode 100644 lib/helpers/serialization/conversation_message_serialization_helper.dart create mode 100644 lib/helpers/serialization/conversations_serialization_helper.dart create mode 100644 lib/models/conversation_member.dart create mode 100644 lib/models/new_conversation.dart create mode 100644 lib/models/new_conversation_settings.dart diff --git a/lib/constants.dart b/lib/constants.dart new file mode 100644 index 0000000..8a4f19d --- /dev/null +++ b/lib/constants.dart @@ -0,0 +1,6 @@ +/// Comunic mobile constants +/// +/// @author Pierre Hubert + +/// Data serialisation directory +const SERIALIZATION_DIRECTORY = "serialization"; \ No newline at end of file diff --git a/lib/helpers/conversations_helper.dart b/lib/helpers/conversations_helper.dart index 44e792d..4615b9f 100644 --- a/lib/helpers/conversations_helper.dart +++ b/lib/helpers/conversations_helper.dart @@ -1,5 +1,5 @@ -import 'package:comunic/helpers/database/conversation_messages_database_helper.dart'; -import 'package:comunic/helpers/database/conversations_database_helper.dart'; +import 'package:comunic/helpers/serialization/conversation_message_serialization_helper.dart'; +import 'package:comunic/helpers/serialization/conversations_serialization_helper.dart'; import 'package:comunic/helpers/users_helper.dart'; import 'package:comunic/helpers/websocket_helper.dart'; import 'package:comunic/lists/conversation_messages_list.dart'; @@ -9,9 +9,12 @@ import 'package:comunic/lists/users_list.dart'; 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_member.dart'; import 'package:comunic/models/conversation_message.dart'; import 'package:comunic/models/displayed_content.dart'; +import 'package:comunic/models/new_conversation.dart'; import 'package:comunic/models/new_conversation_message.dart'; +import 'package:comunic/models/new_conversation_settings.dart'; import 'package:comunic/models/unread_conversation.dart'; import 'package:comunic/utils/account_utils.dart'; import 'package:meta/meta.dart'; @@ -25,20 +28,16 @@ enum SendMessageResult { SUCCESS, MESSAGE_REJECTED, FAILED } class ConversationsHelper { static final _registeredConversations = Map(); - final ConversationsDatabaseHelper _conversationsDatabaseHelper = - ConversationsDatabaseHelper(); - final ConversationMessagesDatabaseHelper _conversationMessagesDatabaseHelper = - ConversationMessagesDatabaseHelper(); - /// Create a new conversation /// /// Return the ID of the newly created conversation or -1 in case of failure - Future createConversation(Conversation settings) async { + Future createConversation(NewConversation settings) async { final response = await APIRequest(uri: "conversations/create", needLogin: true, args: { - "name": settings.hasName ? settings.name : "false", - "follow": settings.following ? "true" : "false", + "name": settings.name ?? "", + "follow": settings.follow ? "true" : "false", "users": settings.members.join(","), + "color": settings.color ?? "," }).addBool("canEveryoneAddMembers", settings.canEveryoneAddMembers).exec(); if (response.code != 200) return -1; @@ -48,72 +47,51 @@ class ConversationsHelper { /// Update an existing conversation /// - /// Returns a boolean depending of the success of the operation - Future updateConversation(Conversation settings) async { - final request = - APIRequest(uri: "conversations/updateSettings", needLogin: true, args: { - "conversationID": settings.id.toString(), - "following": settings.following ? "true" : "false" - }); + /// Throws in case of failure + Future updateConversation(NewConversationsSettings settings) async { + final request = APIRequest.withLogin("conversations/updateSettings") + .addInt("conversationID", settings.convID) + .addBool("following", settings.following); - if (settings.isOwner || settings.canEveryoneAddMembers) - request.addString("members", settings.members.join(",")); + // Update conversation settings + if (settings.isComplete) + request + .addString("name", settings.name ?? "") + .addBool("canEveryoneAddMembers", settings.canEveryoneAddMembers) + .addString("color", settings.color ?? ""); - // Update all conversation settings, if possible - if (settings.isOwner) { - request.addString("name", settings.hasName ? settings.name : "false"); - request.addBool("canEveryoneAddMembers", settings.canEveryoneAddMembers); - } + await request.execWithThrow(); - final response = await request.exec(); - - if (response.code != 200) return false; - - //Delete old conversation entry from the database - await _conversationsDatabaseHelper.delete(settings.id); - - // Success - return true; + // Delete old conversation entry from the database + await ConversationsSerializationHelper() + .removeElement((t) => t.id == settings.convID); } /// Delete a conversation specified by its [id] - Future deleteConversation(int id) async { - final response = await APIRequest( - uri: "conversations/delete", - needLogin: true, - args: { - "conversationID": id.toString(), - }, - ).exec(); - - return response.code == 200; - } + Future deleteConversation(int id) async => + await APIRequest.withLogin("conversations/delete") + .addInt("conversationID", id) + .execWithThrow(); /// Download the list of conversations from the server + /// + /// Throws an exception in case of failure Future downloadList() async { final response = - await APIRequest(uri: "conversations/getList", needLogin: true).exec(); + await APIRequest.withLogin("conversations/getList").execWithThrow(); - if (response.code != 200) return null; + ConversationsList list = ConversationsList(); + response.getArray().forEach((f) => list.add(apiToConversation(f))); - try { - ConversationsList list = ConversationsList(); - response.getArray().forEach((f) => list.add(apiToConversation(f))); + // Update the database + await ConversationsSerializationHelper().setList(list); - // Update the database - await _conversationsDatabaseHelper.clearTable(); - await _conversationsDatabaseHelper.insertAll(list); - - return list; - } on Exception catch (e) { - print(e.toString()); - return null; - } + return list; } /// Get the local list of conversations Future getCachedList() async { - final list = await _conversationsDatabaseHelper.getAll(); + final list = await ConversationsSerializationHelper().getList(); list.sort(); return list; } @@ -124,41 +102,31 @@ class ConversationsHelper { final response = await APIRequest( uri: "conversations/getInfoOne", needLogin: true, - args: {"conversationID": id.toString()}).exec(); - - if (response.code != 200) return null; + args: {"conversationID": id.toString()}).execWithThrow(); final conversation = apiToConversation(response.getObject()); - _conversationsDatabaseHelper.insertOrUpdate(conversation); + + await ConversationsSerializationHelper() + .insertOrReplaceElement((c) => c.id == conversation.id, conversation); return conversation; - } on Exception catch (e) { - print(e.toString()); - print("Could not get information about a single conversation !"); + } on Exception catch (e, s) { + print("Could not get information about a single conversation ! $e => $s"); return null; } } - /// Get information about a single conversation. If [force] is set to false, + /// Get information about a conversation. If [force] is set to false, a /// cached version of the conversation will be used, else it will always get - /// the information from the server - Future getSingle(int id, {bool force = false}) async { - if (force || !await _conversationsDatabaseHelper.has(id)) - return await _downloadSingle(id); - else - return _conversationsDatabaseHelper.get(id); - } - - /// Get information about a conversation. The method throws an [Exception] in + /// the information from the server. The method throws an [Exception] in /// case of failure /// /// Return value of this method is never null. - Future getSingleOrThrow(int id, {bool force = false}) async { - final conv = await this.getSingle(id, force: force); - - if (conv == null) - throw Exception("Could not get information about the conversation!"); - - return conv; + Future getSingle(int id, {bool force = false}) async { + if (force || + !await ConversationsSerializationHelper().any((c) => c.id == id)) + return await _downloadSingle(id); + else + return await ConversationsSerializationHelper().get(id); } /// Get the name of a [conversation]. This requires information @@ -170,9 +138,9 @@ class ConversationsHelper { String name = ""; int count = 0; for (int i = 0; i < 3 && i < conversation.members.length; i++) - if (conversation.members[i] != userID()) { + if (conversation.members[i].userID != userID()) { name += (count > 0 ? ", " : "") + - users.getUser(conversation.members[i]).fullName; + users.getUser(conversation.members[i].userID).fullName; count++; } @@ -184,6 +152,8 @@ class ConversationsHelper { /// Search and return a private conversation with a given [userID]. If such /// conversation does not exists, it is created if [allowCreate] is set to /// true + /// + /// Throws an exception in case of failure Future getPrivate(int userID, {bool allowCreate = true}) async { final response = await APIRequest( uri: "conversations/getPrivate", @@ -192,17 +162,10 @@ class ConversationsHelper { "otherUser": userID.toString(), "allowCreate": allowCreate.toString() }, - ).exec(); - - if (response.code != 200) return null; + ).execWithThrow(); // Get and return conversation ID - try { - return int.parse(response.getObject()["conversationsID"][0].toString()); - } catch (e) { - e.toString(); - return null; - } + return int.parse(response.getObject()["conversationsID"][0].toString()); } /// Asynchronously get the name of the conversation @@ -210,15 +173,13 @@ class ConversationsHelper { /// Unlike the synchronous method, this method does not need information /// about the members of the conversation /// - /// Returns null in case of failure + /// Throws an exception in case of failure static Future getConversationNameAsync( Conversation conversation) async { if (conversation.hasName) return conversation.name; //Get information about the members of the conversation - final members = await UsersHelper().getUsersInfo(conversation.members); - - if (members == null) return null; + final members = await UsersHelper().getList(conversation.membersID); return ConversationsHelper.getConversationName(conversation, members); } @@ -226,14 +187,18 @@ class ConversationsHelper { /// Turn an API entry into a [Conversation] object static Conversation apiToConversation(Map map) { return Conversation( - id: map["ID"], - ownerID: map["ID_owner"], - lastActive: map["last_active"], - name: map["name"] == false ? null : map["name"], - following: map["following"] == 1, - sawLastMessage: map["saw_last_message"] == 1, - members: List.from(map["members"]), - canEveryoneAddMembers: map["canEveryoneAddMembers"], + id: map["id"], + lastActivity: map["last_activity"], + name: map["name"], + color: map["color"], + logoURL: map["logo"], + groupID: map["group_id"], + members: map["members"] + .cast>() + .map(apiToConversationMember) + .toList() + .cast(), + canEveryoneAddMembers: map["can_everyone_add_members"], callCapabilities: map["can_have_video_call"] ? CallCapabilities.VIDEO : (map["can_have_call"] @@ -242,10 +207,21 @@ class ConversationsHelper { isHavingCall: map["has_call_now"]); } + static ConversationMember apiToConversationMember(Map map) => + ConversationMember( + userID: map["user_id"], + lastMessageSeen: map["last_message_seen"], + lastAccessTime: map["last_access"], + following: map["following"], + isAdmin: map["is_admin"], + ); + /// Parse a list of messages given by the server + /// + /// Throws an exception in case of failure Future _parseConversationMessageFromServer( int conversationID, APIResponse response) async { - if (response.code != 200) return null; + response.assertOk(); // Parse the response of the server ConversationMessagesList list = ConversationMessagesList(); @@ -256,7 +232,8 @@ class ConversationsHelper { }); // Save messages in the cache - _conversationMessagesDatabaseHelper.insertOrUpdateAll(list); + await ConversationsMessagesSerializationHelper(conversationID) + .insertOrReplaceAll(list); return list; } @@ -265,6 +242,8 @@ class ConversationsHelper { /// /// Set [lastMessageID] to 0 to specify that we do not have any message of the /// conversation yet or another value else + /// + /// Throws an exception in case of failure Future _downloadNewMessagesSingle( int conversationID, {int lastMessageID = 0}) async { @@ -275,26 +254,26 @@ class ConversationsHelper { args: { "conversationID": conversationID.toString(), "last_message_id": lastMessageID.toString() - }).exec(); + }).execWithThrow(); return await _parseConversationMessageFromServer(conversationID, response); } /// Get older messages for a given conversation from an online source + /// + /// Throws in case of failure Future getOlderMessages({ @required int conversationID, @required int oldestMessagesID, int limit = 15, }) async { // Perform the request online - final response = await APIRequest( - uri: "conversations/get_older_messages", - needLogin: true, - args: { - "conversationID": conversationID.toString(), - "oldest_message_id": oldestMessagesID.toString(), - "limit": limit.toString() - }).exec(); + final response = + await APIRequest.withLogin("conversations/get_older_messages", args: { + "conversationID": conversationID.toString(), + "oldest_message_id": oldestMessagesID.toString(), + "limit": limit.toString() + }).execWithThrow(); return await _parseConversationMessageFromServer(conversationID, response); } @@ -304,6 +283,8 @@ class ConversationsHelper { /// If [lastMessageID] is set to 0 then we retrieve the last messages of /// the conversation. /// Otherwise [lastMessageID] contains the ID of the last known message + /// + /// Throws in case of failure Future getNewMessages( {@required int conversationID, int lastMessageID = 0, @@ -312,16 +293,8 @@ class ConversationsHelper { return await _downloadNewMessagesSingle(conversationID, lastMessageID: lastMessageID); else - return await _conversationMessagesDatabaseHelper - .getAllMessagesConversations(conversationID, - 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); + return await ConversationsMessagesSerializationHelper(conversationID) + .getList(); } /// Send a new message to the server @@ -353,14 +326,13 @@ class ConversationsHelper { } /// Save / Update a message into the database - Future saveMessage(ConversationMessage msg) async { - await _conversationMessagesDatabaseHelper.insertOrUpdate(msg); - } + Future saveMessage(ConversationMessage msg) async => + await ConversationsMessagesSerializationHelper(msg.convID) + .insertOrReplace(msg); /// Remove a message from the database - Future removeMessage(int msgID) async { - await _conversationMessagesDatabaseHelper.delete(msgID); - } + Future removeMessage(ConversationMessage msg) async => + await ConversationsMessagesSerializationHelper(msg.convID).remove(msg); /// Update a message content Future updateMessage(int id, String newContent) async { @@ -397,11 +369,8 @@ class ConversationsHelper { return UnreadConversationsList() ..addAll(list.map((f) => UnreadConversation( - id: f["id"], - convName: f["conv_name"], - lastActive: f["last_active"], - userID: f["userID"], - message: f["message"], + conv: apiToConversation(f["conv"]), + message: apiToConversationMessage(f["message"]), ))); } @@ -432,13 +401,38 @@ class ConversationsHelper { static ConversationMessage apiToConversationMessage( Map map, ) { + var file; + if (map["file"] != null) { + final fileMap = map["file"]; + file = ConversationMessageFile( + url: fileMap["url"], + size: fileMap["size"], + name: fileMap["name"], + thumbnail: fileMap["thumbnail"], + type: fileMap["type"], + ); + } + + var serverMessage; + if (map["server_message"] != null) { + final srvMessageMap = map["server_message"]; + serverMessage = ConversationServerMessage( + type: srvMessageMap["type"], + userID: srvMessageMap["user_id"], + userWhoAdded: srvMessageMap["user_who_added"], + userAdded: srvMessageMap["user_added"], + userWhoRemoved: srvMessageMap["user_who_removed"], + userRemoved: srvMessageMap["user_removed"], + ); + } + return ConversationMessage( - id: map["ID"], - conversationID: map["convID"], - userID: map["ID_user"], - timeInsert: map["time_insert"], - message: DisplayedString(map["message"]), - imageURL: map["image_path"], - ); + id: map["id"], + convID: map["conv_id"], + userID: map["user_id"], + timeSent: map["time_sent"], + message: DisplayedString(map["message"] ?? ""), + file: file, + serverMessage: serverMessage); } } diff --git a/lib/helpers/database/conversation_messages_database_helper.dart b/lib/helpers/database/conversation_messages_database_helper.dart deleted file mode 100644 index 7a30f95..0000000 --- a/lib/helpers/database/conversation_messages_database_helper.dart +++ /dev/null @@ -1,37 +0,0 @@ -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'; - -/// Conversation messages database helper -/// -/// @author Pierre HUBERT - -class ConversationMessagesDatabaseHelper - extends ModelDatabaseHelper { - @override - ConversationMessage initializeFromMap(Map map) { - return ConversationMessage.fromMap(map); - } - - @override - String tableName() { - return ConversationsMessagesTableContract.TABLE_NAME; - } - - /// Get all the message cached for a given conversation - Future getAllMessagesConversations( - int conversationID, - {int lastMessageID = 0}) async { - final list = await getMultiple( - where: "${ConversationsMessagesTableContract.C_CONVERSATION_ID} = ? " - "AND ${BaseTableContract.C_ID} > ?", - whereArgs: [conversationID, lastMessageID], - ); - - // Turn the list into a conversation messages list - ConversationMessagesList finalList = ConversationMessagesList(); - finalList.addAll(list); - return finalList; - } -} diff --git a/lib/helpers/database/conversations_database_helper.dart b/lib/helpers/database/conversations_database_helper.dart deleted file mode 100644 index 29e146e..0000000 --- a/lib/helpers/database/conversations_database_helper.dart +++ /dev/null @@ -1,29 +0,0 @@ -import 'package:comunic/helpers/database/database_contract.dart'; -import 'package:comunic/helpers/database/model_database_helper.dart'; -import 'package:comunic/lists/conversations_list.dart'; -import 'package:comunic/models/conversation.dart'; - -/// Conversations database helper -/// -/// @author Pierre HUBERT - -class ConversationsDatabaseHelper extends ModelDatabaseHelper { - @override - Conversation initializeFromMap(Map map) { - return Conversation.fromMap(map); - } - - @override - String tableName() { - return ConversationTableContract.TABLE_NAME; - } - - @override - Future getAll() async { - ConversationsList list = ConversationsList(); - list.addAll(await super.getAll()); - return list; - } - - -} \ No newline at end of file diff --git a/lib/helpers/database/database_contract.dart b/lib/helpers/database/database_contract.dart index daca551..b41823c 100644 --- a/lib/helpers/database/database_contract.dart +++ b/lib/helpers/database/database_contract.dart @@ -25,30 +25,6 @@ abstract class UserTableContract { static const C_CUSTOM_EMOJIES = "custom_emojies"; } -/// Conversations table contract -abstract class ConversationTableContract { - static const TABLE_NAME = "conversations"; - static const C_ID = BaseTableContract.C_ID; - static const C_OWNER_ID = "owner_id"; - static const C_LAST_ACTIVE = "last_active"; - static const C_NAME = "name"; - static const C_FOLLOWING = "following"; - static const C_SAW_LAST_MESSAGE = "saw_last_message"; - static const C_MEMBERS = "members"; - static const C_CAN_EVERYONE_ADD_MEMBERS = "can_everyone_add_members"; -} - -/// Conversations messages table contract -abstract class ConversationsMessagesTableContract { - static const TABLE_NAME = "conversations_messages"; - static const C_ID = BaseTableContract.C_ID; - static const C_CONVERSATION_ID = "conversation_id"; - static const C_USER_ID = "user_id"; - static const C_TIME_INSERT = "time_insert"; - static const C_MESSAGE = "message"; - static const C_IMAGE_URL = "image_url"; -} - /// Friends table contract abstract class FriendsListTableContract { static const TABLE_NAME = "friends"; diff --git a/lib/helpers/database/database_helper.dart b/lib/helpers/database/database_helper.dart index 0a85afa..8048bad 100644 --- a/lib/helpers/database/database_helper.dart +++ b/lib/helpers/database/database_helper.dart @@ -45,14 +45,6 @@ abstract class DatabaseHelper { // Drop users table await db.execute("DROP TABLE IF EXISTS ${UserTableContract.TABLE_NAME}"); - // Drop conversations table - await db.execute( - "DROP TABLE IF EXISTS ${ConversationTableContract.TABLE_NAME}"); - - // Drop conversations messages table - await db.execute( - "DROP TABLE IF EXISTS ${ConversationsMessagesTableContract.TABLE_NAME}"); - // Drop friends list table await db .execute("DROP TABLE IF EXISTS ${FriendsListTableContract.TABLE_NAME}"); @@ -74,29 +66,6 @@ abstract class DatabaseHelper { "${UserTableContract.C_CUSTOM_EMOJIES} TEXT" ")"); - // Create conversations table - await db.execute("CREATE TABLE ${ConversationTableContract.TABLE_NAME} (" - "${ConversationTableContract.C_ID} INTEGER PRIMARY KEY, " - "${ConversationTableContract.C_OWNER_ID} INTEGER, " - "${ConversationTableContract.C_LAST_ACTIVE} INTEGER, " - "${ConversationTableContract.C_NAME} TEXT, " - "${ConversationTableContract.C_FOLLOWING} INTEGER, " - "${ConversationTableContract.C_SAW_LAST_MESSAGE} INTEGER, " - "${ConversationTableContract.C_MEMBERS} TEXT, " - "${ConversationTableContract.C_CAN_EVERYONE_ADD_MEMBERS} INTEGER" - ")"); - - // Create conversation messages table - await db.execute( - "CREATE TABLE ${ConversationsMessagesTableContract.TABLE_NAME} (" - "${ConversationsMessagesTableContract.C_ID} INTEGER PRIMARY KEY, " - "${ConversationsMessagesTableContract.C_CONVERSATION_ID} INTEGER, " - "${ConversationsMessagesTableContract.C_USER_ID} INTEGER, " - "${ConversationsMessagesTableContract.C_TIME_INSERT} INTEGER, " - "${ConversationsMessagesTableContract.C_MESSAGE} TEXT, " - "${ConversationsMessagesTableContract.C_IMAGE_URL} TEXT" - ")"); - // Friends list table await db.execute("CREATE TABLE ${FriendsListTableContract.TABLE_NAME} (" "${FriendsListTableContract.C_ID} INTEGER PRIMARY KEY, " diff --git a/lib/helpers/serialization/base_serialization_helper.dart b/lib/helpers/serialization/base_serialization_helper.dart new file mode 100644 index 0000000..dbba67b --- /dev/null +++ b/lib/helpers/serialization/base_serialization_helper.dart @@ -0,0 +1,121 @@ +import 'dart:convert'; +import 'dart:io'; + +import 'package:comunic/constants.dart'; +import 'package:path/path.dart' as path; +import 'package:path_provider/path_provider.dart'; + +/// Base serialization helper +/// +/// @author Pierre Hubert + +abstract class SerializableElement extends Comparable { + Map toJson(); +} + +abstract class BaseSerializationHelper { + /// List cache + List _cache; + + /// The name of the type of data to serialise + String get type; + + /// Parse an json entry into a [T] object + T parse(Map m); + + /// Get the file where data should be stored + Future _getFilePath() async { + final dir = await getApplicationDocumentsDirectory(); + final targetDir = + Directory(path.join(dir.absolute.path, SERIALIZATION_DIRECTORY)); + + targetDir.create(recursive: true); + + return File(path.join(targetDir.absolute.path, type)); + } + + /// Load the cache + Future _loadCache() async { + if (_cache != null) return; + + try { + final file = await _getFilePath(); + + if (!await file.exists()) return _cache = List(); + + final List json = jsonDecode(await file.readAsString()); + _cache = json.cast>().map(parse).toList(); + + _cache.sort(); + } catch (e, s) { + print("Failed to read serialized data! $e => $s"); + _cache = List(); + } + } + + /// Save the cache to the persistent memory + Future _saveCache() async { + final file = await _getFilePath(); + await file.writeAsString(jsonEncode( + _cache.map((e) => e.toJson()).toList().cast>())); + } + + /// Get the current list of elements + Future> getList() async { + await _loadCache(); + return List.from(_cache); + } + + /// Set a new list of conversations + Future setList(List list) async { + _cache = List.from(list); + await _saveCache(); + } + + /// Insert new element + Future insert(T el) async { + await _loadCache(); + _cache.add(el); + _cache.sort(); + await _saveCache(); + } + + /// Insert new element + Future insertMany(List els) async { + await _loadCache(); + _cache.addAll(els); + _cache.sort(); + await _saveCache(); + } + + /// Check if any entry in the last match the predicate + Future any(bool isContained(T t)) async { + await _loadCache(); + return _cache.any((element) => isContained(element)); + } + + /// Check if any entry in the last match the predicate + Future first(bool filter(T t)) async { + await _loadCache(); + return _cache.firstWhere((element) => filter(element)); + } + + /// Replace an element with another one + Future insertOrReplaceElement(bool isToReplace(T t), T newEl) async { + await _loadCache(); + + // Insert or replace the element + _cache.where((element) => !isToReplace(element)).toList(); + _cache.add(newEl); + + _cache.sort(); + await _saveCache(); + } + + /// Remove elements + Future removeElement(bool isToRemove(T t)) async { + await _loadCache(); + _cache.removeWhere((element) => isToRemove(element)); + await _saveCache(); + } +} diff --git a/lib/helpers/serialization/conversation_message_serialization_helper.dart b/lib/helpers/serialization/conversation_message_serialization_helper.dart new file mode 100644 index 0000000..16ba7df --- /dev/null +++ b/lib/helpers/serialization/conversation_message_serialization_helper.dart @@ -0,0 +1,50 @@ +import 'dart:collection'; + +import 'package:comunic/helpers/serialization/base_serialization_helper.dart'; +import 'package:comunic/lists/conversation_messages_list.dart'; +import 'package:comunic/models/conversation_message.dart'; + +/// Conversations serialization helper +/// +/// @author Pierre Hubert + +final HashMap _instances = + HashSet() as HashMap; + +class ConversationsMessagesSerializationHelper + extends BaseSerializationHelper { + final int convID; + + ConversationsMessagesSerializationHelper._(int convID) + : convID = convID, + assert(convID != null); + + factory ConversationsMessagesSerializationHelper(int convID) { + if (!_instances.containsKey(convID)) + _instances[convID] = ConversationsMessagesSerializationHelper._(convID); + + return _instances[convID]; + } + + @override + ConversationMessage parse(Map m) => + ConversationMessage.fromJson(m); + + @override + String get type => "conv-messages-$convID"; + + Future getList() async => + ConversationMessagesList()..addAll(await super.getList()); + + Future insertOrReplace(ConversationMessage msg) async => + await insertOrReplaceElement((t) => t.id == msg.id, msg); + + Future remove(ConversationMessage msg) async => + await removeElement((t) => t.id == msg.id); + + /// Insert or replace a list of messages + Future insertOrReplaceAll(List list) async { + for (var message in list) + await insertOrReplaceElement((t) => t.id == message.id, message); + } +} diff --git a/lib/helpers/serialization/conversations_serialization_helper.dart b/lib/helpers/serialization/conversations_serialization_helper.dart new file mode 100644 index 0000000..30ffbfe --- /dev/null +++ b/lib/helpers/serialization/conversations_serialization_helper.dart @@ -0,0 +1,32 @@ +import 'package:comunic/helpers/serialization/base_serialization_helper.dart'; +import 'package:comunic/lists/conversations_list.dart'; +import 'package:comunic/models/conversation.dart'; + +/// Conversations serialization helper +/// +/// @author Pierre Hubert + +var _cache; + +class ConversationsSerializationHelper + extends BaseSerializationHelper { + /// Singleton + factory ConversationsSerializationHelper() { + if (_cache == null) _cache = ConversationsSerializationHelper._(); + return _cache; + } + + ConversationsSerializationHelper._(); + + @override + Conversation parse(Map m) => Conversation.fromJson(m); + + @override + String get type => "conversations"; + + Future getList() async => + ConversationsList()..addAll(await super.getList()); + + /// Get a conversation + Future get(int id) => first((t) => t.id == id); +} diff --git a/lib/helpers/users_helper.dart b/lib/helpers/users_helper.dart index d4b4eef..193e842 100644 --- a/lib/helpers/users_helper.dart +++ b/lib/helpers/users_helper.dart @@ -91,9 +91,16 @@ class UsersHelper { } /// Get users information from a given [Set] + /// + /// Throws in case of failure Future getList(Set users, {bool forceDownload = false}) async { - return await getUsersInfo(users.toList()); + final list = await getUsersInfo(users.toList()); + + if (list == null) + throw Exception("Failed to get the list of users!"); + + return list; } /// Get users information diff --git a/lib/lists/conversations_list.dart b/lib/lists/conversations_list.dart index 0b47bce..cd02f27 100644 --- a/lib/lists/conversations_list.dart +++ b/lib/lists/conversations_list.dart @@ -8,11 +8,11 @@ import 'package:comunic/models/conversation.dart'; /// @author Pierre HUBERT class ConversationsList extends ListBase { - final List _list = List(); UsersList users; set length(l) => _list.length = l; + int get length => _list.length; @override @@ -22,12 +22,9 @@ class ConversationsList extends ListBase { void operator []=(int index, Conversation value) => _list[index] = value; /// Get the entire lists of users ID in this list - List get allUsersID { - final List list = List(); - forEach((c) => c.members.forEach((id){ - if(!list.contains(id)) - list.add(id); - })); + Set get allUsersID { + final Set list = Set(); + forEach((c) => c.members.forEach((member) => list.add(member.userID))); return list; } } diff --git a/lib/lists/memberships_list.dart b/lib/lists/memberships_list.dart index 03e90bb..dcfa001 100644 --- a/lib/lists/memberships_list.dart +++ b/lib/lists/memberships_list.dart @@ -18,7 +18,7 @@ class MembershipList extends AbstractList { case MembershipType.GROUP: break; case MembershipType.CONVERSATION: - s.addAll(m.conversation.members); + s.addAll(m.conversation.membersID); break; } }); diff --git a/lib/lists/unread_conversations_list.dart b/lib/lists/unread_conversations_list.dart index 6ae604d..21431bc 100644 --- a/lib/lists/unread_conversations_list.dart +++ b/lib/lists/unread_conversations_list.dart @@ -7,5 +7,9 @@ import 'package:comunic/models/unread_conversation.dart'; class UnreadConversationsList extends AbstractList { /// Get the ID of the users included in this list - Set get usersID => new Set()..addAll(map((f) => f.userID)); + Set get usersID { + final set = Set(); + forEach((element) => set.addAll(element.message.usersID)); + return set; + } } diff --git a/lib/models/conversation.dart b/lib/models/conversation.dart index d703186..08d496e 100644 --- a/lib/models/conversation.dart +++ b/lib/models/conversation.dart @@ -1,7 +1,6 @@ -import 'package:comunic/helpers/database/database_contract.dart'; -import 'package:comunic/models/cache_model.dart'; +import 'package:comunic/helpers/serialization/base_serialization_helper.dart'; +import 'package:comunic/models/conversation_member.dart'; import 'package:comunic/utils/account_utils.dart'; -import 'package:comunic/utils/list_utils.dart'; import 'package:meta/meta.dart'; /// Conversation model @@ -10,79 +9,86 @@ import 'package:meta/meta.dart'; enum CallCapabilities { NONE, AUDIO, VIDEO } -class Conversation extends CacheModel implements Comparable { - final int ownerID; - final int lastActive; +class Conversation extends SerializableElement { + final int id; + final int lastActivity; final String name; - final bool following; - final bool sawLastMessage; - final List members; + final String color; + final String logoURL; + final int groupID; + final List members; final bool canEveryoneAddMembers; final CallCapabilities callCapabilities; final bool isHavingCall; - const Conversation({ - @required int id, - @required this.ownerID, - @required this.lastActive, + Conversation({ + @required this.id, + @required this.lastActivity, @required this.name, - @required this.following, - @required this.sawLastMessage, + @required this.color, + @required this.logoURL, + @required this.groupID, @required this.members, @required this.canEveryoneAddMembers, this.callCapabilities = CallCapabilities.NONE, this.isHavingCall = false, }) : assert(id != null), - assert(ownerID != null), - assert(lastActive != null), - assert(following != null), - assert(sawLastMessage != null), + assert(lastActivity != null), assert(members != null), assert(canEveryoneAddMembers != null), assert(callCapabilities != null), - assert(isHavingCall != null), - super(id: id); + assert(isHavingCall != null); /// Check out whether a conversation has a fixed name or not bool get hasName => this.name != null; - /// Check out whether current user of the application is the owner of it or - /// not - bool get isOwner => this.ownerID == userID(); + /// Get current user membership + ConversationMember get membership => + members.firstWhere((m) => m.userID == userID()); - Conversation.fromMap(Map map) - : ownerID = map[ConversationTableContract.C_OWNER_ID], - lastActive = map[ConversationTableContract.C_LAST_ACTIVE], - name = map[ConversationTableContract.C_NAME], - following = map[ConversationTableContract.C_FOLLOWING] == 1, - sawLastMessage = map[ConversationTableContract.C_SAW_LAST_MESSAGE] == 1, - members = - listToIntList(map[ConversationTableContract.C_MEMBERS].split(",")), - canEveryoneAddMembers = - map[ConversationTableContract.C_CAN_EVERYONE_ADD_MEMBERS] == 1, + /// Check out whether current user of the application is an admin + bool get isAdmin => membership.isAdmin; + + /// Check it current user is following the conversation or not + bool get following => membership.following; + + /// Get the list of members in the conversation + Set get membersID => members.map((e) => e.userID).toSet(); + + /// Check if the last message has been seen or not + bool get sawLastMessage => lastActivity <= membership.lastAccessTime; + + Conversation.fromJson(Map map) + : id = map["id"], + name = map["name"], + color = map["color"], + logoURL = map["logoURL"], + groupID = map["groupID"], + lastActivity = map["lastActivity"], + members = map["members"] + .map((el) => ConversationMember.fromJSON(el)) + .toList(), + canEveryoneAddMembers = map["canEveryoneAddMembers"], // By default, we can not do any call callCapabilities = CallCapabilities.NONE, - isHavingCall = false, - super.fromMap(map); + isHavingCall = false; - @override - Map toMap() { + Map toJson() { return { - ConversationTableContract.C_ID: id, - ConversationTableContract.C_OWNER_ID: ownerID, - ConversationTableContract.C_LAST_ACTIVE: lastActive, - ConversationTableContract.C_NAME: name, - ConversationTableContract.C_FOLLOWING: following ? 1 : 0, - ConversationTableContract.C_SAW_LAST_MESSAGE: sawLastMessage ? 1 : 0, - ConversationTableContract.C_MEMBERS: members.join(","), - ConversationTableContract.C_CAN_EVERYONE_ADD_MEMBERS: - canEveryoneAddMembers ? 1 : 0 + "id": id, + "name": name, + "color": color, + "logoURL": logoURL, + "groupID": groupID, + "lastActivity": lastActivity, + "members": members.map((e) => e.toJson()).toList(), + "canEveryoneAddMembers": canEveryoneAddMembers, }; } @override - int compareTo(other) { - return other.lastActive.compareTo(this.lastActive); + int compareTo(Conversation other) { + return other.lastActivity.compareTo(this.lastActivity); } } diff --git a/lib/models/conversation_member.dart b/lib/models/conversation_member.dart new file mode 100644 index 0000000..f62c7a2 --- /dev/null +++ b/lib/models/conversation_member.dart @@ -0,0 +1,40 @@ +import 'package:flutter/widgets.dart'; + +/// Conversation member +/// +/// @author Pierre Hubert + +class ConversationMember { + final int userID; + final int lastMessageSeen; + final int lastAccessTime; + final bool following; + final bool isAdmin; + + const ConversationMember({ + @required this.userID, + @required this.lastMessageSeen, + @required this.lastAccessTime, + @required this.following, + @required this.isAdmin, + }) : assert(userID != null), + assert(lastMessageSeen != null), + assert(lastAccessTime != null), + assert(following != null), + assert(isAdmin != null); + + Map toJson() => { + 'userID': userID, + 'lastMessageSeen': lastMessageSeen, + 'lastAccessTime': lastAccessTime, + 'following': following, + 'isAdmin': isAdmin, + }; + + ConversationMember.fromJSON(Map json) + : userID = json["userID"], + lastMessageSeen = json["lastMessageSeen"], + lastAccessTime = json["lastAccessTime"], + following = json["following"], + isAdmin = json["isAdmin"]; +} diff --git a/lib/models/conversation_message.dart b/lib/models/conversation_message.dart index c21c64b..3e7de0a 100644 --- a/lib/models/conversation_message.dart +++ b/lib/models/conversation_message.dart @@ -1,5 +1,4 @@ -import 'package:comunic/helpers/database/database_contract.dart'; -import 'package:comunic/models/cache_model.dart'; +import 'package:comunic/helpers/serialization/base_serialization_helper.dart'; import 'package:comunic/models/displayed_content.dart'; import 'package:comunic/utils/account_utils.dart' as account; import 'package:meta/meta.dart'; @@ -8,59 +7,171 @@ import 'package:meta/meta.dart'; /// /// @author Pierre HUBERT -class ConversationMessage extends CacheModel implements Comparable { - final int id; - final int conversationID; +class ConversationMessageFile { + final String url; + final int size; + final String name; + final String thumbnail; + final String type; + + const ConversationMessageFile({ + @required this.url, + @required this.size, + @required this.name, + @required this.thumbnail, + @required this.type, + }) : assert(url != null), + assert(size != null), + assert(name != null), + assert(type != null); + + Map toJson() => { + "url": url, + "size": size, + "name": name, + "thumbnail": thumbnail, + "type": type + }; + + ConversationMessageFile.fromJson(Map json) + : url = json["url"], + size = json["size"], + name = json["name"], + thumbnail = json["thumbnail"], + type = json["type"]; +} + +enum ConversationServerMessageType { + USER_CREATED_CONVERSATION, + USER_ADDED_ANOTHER_USER, + USER_LEFT_CONV, + USER_REMOVED_ANOTHER_USER +} + +class ConversationServerMessage { + final ConversationServerMessageType type; final int userID; - final int timeInsert; - final DisplayedString message; - final String imageURL; + final int userWhoAdded; + final int userAdded; + final int userWhoRemoved; + final int userRemoved; - const ConversationMessage({ - @required this.id, - @required this.conversationID, + const ConversationServerMessage({ + @required this.type, @required this.userID, - @required this.timeInsert, - @required this.message, - @required this.imageURL, - }) : assert(id != null), - assert(userID != null), - assert(timeInsert != null), - assert(message != null), - super(id: id); + @required this.userWhoAdded, + @required this.userAdded, + @required this.userWhoRemoved, + @required this.userRemoved, + }) : assert(type != null), + assert(userID != null || + (type != ConversationServerMessageType.USER_CREATED_CONVERSATION && + type != ConversationServerMessageType.USER_LEFT_CONV)), + assert((userWhoAdded != null && userAdded != null) || + type != ConversationServerMessageType.USER_ADDED_ANOTHER_USER), + assert((userWhoRemoved != null && userRemoved != null) || + type != ConversationServerMessageType.USER_REMOVED_ANOTHER_USER); - DateTime get date => DateTime.fromMillisecondsSinceEpoch(timeInsert * 1000); + Set get usersID { + switch (type) { + case ConversationServerMessageType.USER_CREATED_CONVERSATION: + case ConversationServerMessageType.USER_LEFT_CONV: + return Set()..add(userID); + + case ConversationServerMessageType.USER_ADDED_ANOTHER_USER: + return Set()..add(userWhoAdded)..add(userAdded); + + case ConversationServerMessageType.USER_REMOVED_ANOTHER_USER: + return Set()..add(userWhoRemoved)..add(userRemoved); + } + + throw Exception("Unsupported server message type!"); + } + + Map toJson() => { + "type": type.toString(), + "userID": userID, + "userWhoAdded": userWhoAdded, + "userAdded": userAdded, + "userWhoRemoved": userWhoRemoved, + "userRemoved": userRemoved, + }; + + ConversationServerMessage.fromJson(Map json) + : type = ConversationServerMessageType.values + .firstWhere((el) => el.toString() == json["type"]), + userID = json["userID"], + userWhoAdded = json["userWhoAdded"], + userAdded = json["userAdded"], + userWhoRemoved = json["userWhoRemoved"], + userRemoved = json["userRemoved"]; +} + +class ConversationMessage extends SerializableElement { + final int id; + final int convID; + final int userID; + final int timeSent; + final DisplayedString message; + final ConversationMessageFile file; + final ConversationServerMessage serverMessage; + + ConversationMessage({ + @required this.id, + @required this.convID, + @required this.userID, + @required this.timeSent, + @required this.message, + @required this.file, + @required this.serverMessage, + }) : assert(id != null), + assert(convID != null), + assert(userID != null), + assert(timeSent != null), + assert(message != null || file != null || serverMessage != null); + + DateTime get date => DateTime.fromMillisecondsSinceEpoch(timeSent * 1000); bool get hasMessage => !message.isNull && message.length > 0; - bool get hasImage => imageURL != null && imageURL != "null"; + bool get hasFile => file != null; + + bool get hasThumbnail => hasFile && file.thumbnail != null; + + bool get hasImage => hasFile && file.type.startsWith("image/"); bool get isOwner => account.userID() == userID; + /// Get the list of the ID of the users implied in this message + Set get usersID { + if (userID != null) return Set()..add(userID); + + return serverMessage.usersID; + } + @override - int compareTo(other) { + int compareTo(ConversationMessage other) { return id.compareTo(other.id); } - @override - Map toMap() { + Map toJson() { return { - ConversationsMessagesTableContract.C_ID: id, - ConversationsMessagesTableContract.C_CONVERSATION_ID: conversationID, - ConversationsMessagesTableContract.C_USER_ID: userID, - ConversationsMessagesTableContract.C_TIME_INSERT: timeInsert, - ConversationsMessagesTableContract.C_MESSAGE: message.content, - ConversationsMessagesTableContract.C_IMAGE_URL: imageURL + "id": id, + "convID": convID, + "userID": userID, + "timeSent": timeSent, + "message": message, + "file": file?.toJson(), + "serverMessage": serverMessage?.toJson(), }; } - ConversationMessage.fromMap(Map map) - : id = map[ConversationsMessagesTableContract.C_ID], - conversationID = - map[ConversationsMessagesTableContract.C_CONVERSATION_ID], - userID = map[ConversationsMessagesTableContract.C_USER_ID], - timeInsert = map[ConversationsMessagesTableContract.C_TIME_INSERT], - message = DisplayedString(map[ConversationsMessagesTableContract.C_MESSAGE]), - imageURL = map[ConversationsMessagesTableContract.C_IMAGE_URL], - super.fromMap(map); + ConversationMessage.fromJson(Map map) + : id = map["id"], + convID = map["convID"], + userID = map["userID"], + timeSent = map["timeSent"], + message = DisplayedString(map["message"]), + file = map["file"], + serverMessage = map["serverMessage"]; } diff --git a/lib/models/membership.dart b/lib/models/membership.dart index aeac571..3c66ef3 100644 --- a/lib/models/membership.dart +++ b/lib/models/membership.dart @@ -43,7 +43,7 @@ class Membership { case MembershipType.GROUP: return groupLastActive; case MembershipType.CONVERSATION: - return conversation.lastActive; + return conversation.lastActivity; default: throw Exception("Unreachable statment!"); } diff --git a/lib/models/new_conversation.dart b/lib/models/new_conversation.dart new file mode 100644 index 0000000..61ee070 --- /dev/null +++ b/lib/models/new_conversation.dart @@ -0,0 +1,24 @@ +import 'package:flutter/cupertino.dart'; + +/// New conversation information +/// +/// @author Pierre Hubert + +class NewConversation { + final String name; + final List members; + final bool follow; + final bool canEveryoneAddMembers; + final String color; + + const NewConversation({ + @required this.name, + @required this.members, + @required this.follow, + @required this.canEveryoneAddMembers, + @required this.color, + }) : assert(members != null), + assert(members.length > 0), + assert(follow != null), + assert(canEveryoneAddMembers != null); +} diff --git a/lib/models/new_conversation_settings.dart b/lib/models/new_conversation_settings.dart new file mode 100644 index 0000000..9e5ebac --- /dev/null +++ b/lib/models/new_conversation_settings.dart @@ -0,0 +1,27 @@ +import 'package:flutter/widgets.dart'; + +/// Conversation settings update +/// +/// @author Pierre Hubert + +class NewConversationsSettings { + final int convID; + final bool following; + final bool isComplete; + final String name; + final bool canEveryoneAddMembers; + final String color; + + const NewConversationsSettings({ + @required this.convID, + @required this.following, + @required this.isComplete, + @required this.name, + @required this.canEveryoneAddMembers, + @required this.color, + }) : assert(convID != null), + assert(convID > 0), + assert(following != null), + assert(isComplete != null), + assert(!isComplete && canEveryoneAddMembers != null); +} diff --git a/lib/models/unread_conversation.dart b/lib/models/unread_conversation.dart index 2088e2e..325b8d1 100644 --- a/lib/models/unread_conversation.dart +++ b/lib/models/unread_conversation.dart @@ -1,3 +1,5 @@ +import 'package:comunic/models/conversation.dart'; +import 'package:comunic/models/conversation_message.dart'; import 'package:flutter/material.dart'; /// Unread conversation information @@ -5,21 +7,12 @@ import 'package:flutter/material.dart'; /// @author Pierre Hubert class UnreadConversation { - final int id; - final String convName; - final int lastActive; - final int userID; - final String message; + final Conversation conv; + final ConversationMessage message; const UnreadConversation({ - @required this.id, - @required this.convName, - @required this.lastActive, - @required this.userID, + @required this.conv, @required this.message, - }) : assert(id != null), - assert(convName != null), - assert(lastActive != null), - assert(userID != null), + }) : assert(conv != null), assert(message != null); } diff --git a/lib/ui/routes/conversation_route.dart b/lib/ui/routes/conversation_route.dart index a3af03d..fadf0f0 100644 --- a/lib/ui/routes/conversation_route.dart +++ b/lib/ui/routes/conversation_route.dart @@ -4,6 +4,7 @@ import 'package:comunic/ui/routes/main_route/main_route.dart'; import 'package:comunic/ui/routes/update_conversation_route.dart'; import 'package:comunic/ui/screens/conversation_screen.dart'; import 'package:comunic/ui/widgets/comunic_back_button_widget.dart'; +import 'package:comunic/ui/widgets/safe_state.dart'; import 'package:comunic/utils/intl_utils.dart'; import 'package:comunic/utils/ui_utils.dart'; import 'package:flutter/material.dart'; @@ -25,7 +26,7 @@ class ConversationRoute extends StatefulWidget { State createState() => _ConversationRouteState(); } -class _ConversationRouteState extends State { +class _ConversationRouteState extends SafeState { final ConversationsHelper _conversationsHelper = ConversationsHelper(); Conversation _conversation; String _conversationName; @@ -42,21 +43,22 @@ class _ConversationRouteState extends State { Future _loadConversation() async { setError(false); - _conversation = await _conversationsHelper.getSingle(widget.conversationID, - force: true); + try { + _conversation = await _conversationsHelper + .getSingle(widget.conversationID, force: true); - if (_conversation == null) return setError(true); + if (_conversation == null) return setError(true); - final conversationName = - await ConversationsHelper.getConversationNameAsync(_conversation); + final conversationName = + await ConversationsHelper.getConversationNameAsync(_conversation); - if (!this.mounted) return null; + if (!this.mounted) return null; - if (conversationName == null) return setError(true); - - setState(() { - _conversationName = conversationName; - }); + setState(() => _conversationName = conversationName); + } catch (e, s) { + print("Failed to get conversation name! $e => $s"); + setError(true); + } } void _openSettings() { diff --git a/lib/ui/routes/update_conversation_route.dart b/lib/ui/routes/update_conversation_route.dart index 6bd9dcd..14b03ca 100644 --- a/lib/ui/routes/update_conversation_route.dart +++ b/lib/ui/routes/update_conversation_route.dart @@ -42,19 +42,18 @@ class _UpdateConversationRoute extends State { Future _loadConversation() async { setError(false); - final conversation = await ConversationsHelper() - .getSingle(widget.conversationID, force: true); + try { + final conversation = await ConversationsHelper() + .getSingle(widget.conversationID, force: true); - if (conversation == null) return setError(true); + //Load information about the members of the conversation + _membersInfo = await UsersHelper().getList(conversation.membersID); - //Load information about the members of the conversation - _membersInfo = await UsersHelper().getUsersInfo(conversation.members); - - if (_membersInfo == null) return setError(true); - - setState(() { - _conversation = conversation; - }); + setState(() => _conversation = conversation); + } catch (e, s) { + print("Failed to load conversation information! $e=>$s"); + setError(true); + } } /// Build the body of this widget diff --git a/lib/ui/screens/call_screen.dart b/lib/ui/screens/call_screen.dart index 7177ad9..02d38ac 100644 --- a/lib/ui/screens/call_screen.dart +++ b/lib/ui/screens/call_screen.dart @@ -110,7 +110,7 @@ class _CallScreenState extends SafeState { // First, load information about the conversation _conversation = - await ConversationsHelper().getSingleOrThrow(convID, force: true); + await ConversationsHelper().getSingle(convID, force: true); _convName = await ConversationsHelper.getConversationNameAsync(_conversation); assert(_convName != null); diff --git a/lib/ui/screens/conversation_members_screen.dart b/lib/ui/screens/conversation_members_screen.dart index 94c1bdb..62b1cd5 100644 --- a/lib/ui/screens/conversation_members_screen.dart +++ b/lib/ui/screens/conversation_members_screen.dart @@ -32,8 +32,7 @@ class _ConversationMembersScreenState extends State { Future _refresh() async { _conversation = await ConversationsHelper().getSingle(widget.convID, force: true); - _members = - await UsersHelper().getListWithThrow(_conversation.members.toSet()); + _members = await UsersHelper().getListWithThrow(_conversation.membersID); } @override @@ -55,12 +54,12 @@ class _ConversationMembersScreenState extends State { ); Widget _buildItem(BuildContext context, int index) { - final user = _members.getUser(_conversation.members[index]); + final member = _conversation.members[index]; + final user = _members.getUser(member.userID); return ListTile( leading: AccountImageWidget(user: user), title: Text(user.displayName), - subtitle: - Text(_conversation.ownerID == user.id ? tr("Owner") : tr("Member")), + subtitle: Text(member.isAdmin ? tr("Admin") : tr("Member")), ); } } diff --git a/lib/ui/screens/conversation_screen.dart b/lib/ui/screens/conversation_screen.dart index 2710d7d..6b665c8 100644 --- a/lib/ui/screens/conversation_screen.dart +++ b/lib/ui/screens/conversation_screen.dart @@ -70,9 +70,8 @@ class _ConversationScreenState extends SafeState { }); /// Method called when an error occurred while loading messages - void _errorLoading() { - _setError(_messages == null ? ErrorLevel.MAJOR : ErrorLevel.MINOR); - } + void _errorLoading() => + _setError(_messages == null ? ErrorLevel.MAJOR : ErrorLevel.MINOR); /// Load the first conversations Future _init() async { @@ -86,22 +85,27 @@ class _ConversationScreenState extends SafeState { .registerConversationEvents(widget.conversationID); this.listen((ev) async { - if (ev.msg.conversationID == widget.conversationID) { - await _conversationsHelper.saveMessage(ev.msg); - await _applyNewMessages(ConversationMessagesList()..add(ev.msg)); + if (ev.msg.convID == widget.conversationID) { + try { + await _conversationsHelper.saveMessage(ev.msg); + await _applyNewMessages(ConversationMessagesList()..add(ev.msg)); + } catch (e, s) { + print("Failed to show new message! $e => $s"); + _errorLoading(); + } } }); this.listen((ev) async { - if (ev.msg.conversationID == widget.conversationID) { + if (ev.msg.convID == widget.conversationID) { await _conversationsHelper.saveMessage(ev.msg); setState(() => _messages.replace(ev.msg)); } }); this.listen((ev) async { - if (ev.msg.conversationID == widget.conversationID) { - await _conversationsHelper.removeMessage(ev.msg.id); + if (ev.msg.convID == widget.conversationID) { + await _conversationsHelper.removeMessage(ev.msg); setState(() => _messages.removeMsg(ev.msg.id)); } }); @@ -116,19 +120,23 @@ class _ConversationScreenState extends SafeState { Future _loadMessages(bool online) async { if (!mounted) return; - //First, get the messages - final messages = await _conversationsHelper.getNewMessages( + try { + //First, get the messages + final messages = await _conversationsHelper.getNewMessages( conversationID: widget.conversationID, lastMessageID: _messages == null ? 0 : _messages.lastMessageID, - online: online); + online: online, + ); - if (messages == null) return _errorLoading(); + // In case we are offline and we did not get any message we do not do + // anything (we wait for the online request) + if (messages.length == 0 && !online) return; - // In case we are offline and we did not get any message we do not do - // anything (we wait for the online request) - if (messages.length == 0 && !online) return; - - await _applyNewMessages(messages); + await _applyNewMessages(messages); + } catch (e, s) { + print("Failed to load messages! $e => $s"); + _errorLoading(); + } } /// Get older messages @@ -136,45 +144,43 @@ class _ConversationScreenState extends SafeState { if (_loadingOlderMessages != _OlderMessagesLevel.NONE || _messages == null || _messages.length == 0) return; + try { + // Let's start to load older messages + _setLoadingOlderMessagesState(_OlderMessagesLevel.LOADING); - // Let's start to load older messages - _setLoadingOlderMessagesState(_OlderMessagesLevel.LOADING); + final messages = await _conversationsHelper.getOlderMessages( + conversationID: widget.conversationID, + oldestMessagesID: _messages.firstMessageID); - final messages = await _conversationsHelper.getOlderMessages( - conversationID: widget.conversationID, - oldestMessagesID: _messages.firstMessageID); + // Mark as not loading anymore + _setLoadingOlderMessagesState(_OlderMessagesLevel.NONE); - // Mark as not loading anymore - _setLoadingOlderMessagesState(_OlderMessagesLevel.NONE); + // Check if there is no more unread messages + if (messages.length == 0) { + _setLoadingOlderMessagesState(_OlderMessagesLevel.NO_MORE_AVAILABLE); + return; + } - // Check for errors - if (messages == null) { + // Apply the messages + _applyNewMessages(messages); + } catch (e, s) { + print("Failed to load older messages! $e => $s"); _errorLoading(); - return; } - - // Check if there is no more unread messages - if (messages.length == 0) { - _setLoadingOlderMessagesState(_OlderMessagesLevel.NO_MORE_AVAILABLE); - return; - } - - // Apply the messages - _applyNewMessages(messages); } /// Apply new messages [messages] must not be null + /// + /// Throws in case of failure Future _applyNewMessages(ConversationMessagesList messages) async { // We ignore new messages once the area is no longer visible if (!this.mounted) return; //Then get information about users final usersToGet = - findMissingFromList(_usersInfo.usersID, messages.getUsersID()); + findMissingFromList(_usersInfo.usersID, messages.getUsersID()).toSet(); - final users = await _usersHelper.getUsersInfo(usersToGet); - - if (users == null) _errorLoading(); + final users = await _usersHelper.getList(usersToGet); // Save the new list of messages setState(() { diff --git a/lib/ui/screens/conversations_list_screen.dart b/lib/ui/screens/conversations_list_screen.dart index e371509..e1d4991 100644 --- a/lib/ui/screens/conversations_list_screen.dart +++ b/lib/ui/screens/conversations_list_screen.dart @@ -62,32 +62,24 @@ class _ConversationScreenState extends SafeState { await _loadConversationsList(false); } - void _gotLoadingError() { - setError(_list == null ? LoadErrorLevel.MAJOR : LoadErrorLevel.MINOR); - } - /// Load the list of conversations Future _loadConversationsList(bool cached) async { setError(LoadErrorLevel.NONE); - //Get the list of conversations - var list; - if (cached) - list = await _conversationsHelper.getCachedList(); - else - list = await _conversationsHelper.downloadList(); + try { + ConversationsList list = cached + ? await _conversationsHelper.getCachedList() + : await _conversationsHelper.downloadList(); + assert(list != null); - if (list == null) return _gotLoadingError(); + //Get information about the members of the conversations + list.users = await _usersHelper.getList(list.allUsersID); - //Get information about the members of the conversations - list.users = await _usersHelper.getUsersInfo(list.allUsersID); - - if (list.users == null) return _gotLoadingError(); - - //Save list - setState(() { - _list = list; - }); + setState(() => _list = list); + } catch (e, s) { + debugPrint("Failed to get conversations list! $e => $s", wrapWidth: 1024); + setError(_list == null ? LoadErrorLevel.MAJOR : LoadErrorLevel.MINOR); + } } /// Build an error card @@ -159,9 +151,13 @@ class _ConversationScreenState extends SafeState { if (result == null || !result) return; // Request the conversation to be deleted now - if (!await _conversationsHelper.deleteConversation(conversation.id)) + try { + await _conversationsHelper.deleteConversation(conversation.id); + } catch (e, s) { + print("Failed to delete conversation! $e => $s"); Scaffold.of(context).showSnackBar( SnackBar(content: Text(tr("Could not delete the conversation!")))); + } // Reload the list of conversations _loadConversationsList(false); diff --git a/lib/ui/screens/unread_conversations_screen.dart b/lib/ui/screens/unread_conversations_screen.dart index c4cadb5..233e07f 100644 --- a/lib/ui/screens/unread_conversations_screen.dart +++ b/lib/ui/screens/unread_conversations_screen.dart @@ -3,11 +3,8 @@ import 'package:comunic/helpers/events_helper.dart'; import 'package:comunic/helpers/users_helper.dart'; import 'package:comunic/lists/unread_conversations_list.dart'; import 'package:comunic/lists/users_list.dart'; -import 'package:comunic/ui/routes/main_route/main_route.dart'; -import 'package:comunic/ui/widgets/account_image_widget.dart'; import 'package:comunic/ui/widgets/async_screen_widget.dart'; import 'package:comunic/ui/widgets/safe_state.dart'; -import 'package:comunic/utils/date_utils.dart'; import 'package:comunic/utils/intl_utils.dart'; import 'package:flutter/material.dart'; @@ -70,7 +67,7 @@ class _UnreadConversationsScreenState } Widget _tileBuilder(BuildContext context, int index) { - final conv = _list[index]; + /*final conv = _list[index]; final user = _users.getUser(conv.userID); return ListTile( leading: AccountImageWidget(user: user), @@ -83,9 +80,12 @@ class _UnreadConversationsScreenState style: TextStyle(fontStyle: FontStyle.italic), ), ]), - ), + ) trailing: Text(diffTimeFromNowToStr(conv.lastActive)), onTap: () => MainController.of(context).openConversation(conv.id), - ); + );*/ + + // TODO : reimplement + throw new Exception("unimplemented"); } } diff --git a/lib/ui/screens/update_conversation_screen.dart b/lib/ui/screens/update_conversation_screen.dart index 7d3765b..885f23c 100644 --- a/lib/ui/screens/update_conversation_screen.dart +++ b/lib/ui/screens/update_conversation_screen.dart @@ -1,8 +1,6 @@ -import 'package:comunic/helpers/conversations_helper.dart'; import 'package:comunic/lists/users_list.dart'; import 'package:comunic/models/conversation.dart'; import 'package:comunic/models/user.dart'; -import 'package:comunic/ui/routes/main_route/main_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'; @@ -36,11 +34,11 @@ class _UpdateConversationScreen extends State { get isUpdating => widget.initialSettings != null; - get isOwner => !isUpdating || widget.initialSettings.isOwner; + get isAdmin => !isUpdating || widget.initialSettings.isAdmin; Conversation get _initialSettings => widget.initialSettings; - bool get _canAddMembers => isOwner || _initialSettings.canEveryoneAddMembers; + bool get _canAddMembers => isAdmin || _initialSettings.canEveryoneAddMembers; @override void initState() { @@ -68,7 +66,7 @@ class _UpdateConversationScreen extends State { decoration: InputDecoration( labelText: tr("Conversation name (optionnal)"), alignLabelWithHint: true, - enabled: isOwner, + enabled: isAdmin, ), ), @@ -90,7 +88,7 @@ class _UpdateConversationScreen extends State { children: [ Switch.adaptive( value: _canEveryoneAddMembers, - onChanged: isOwner + onChanged: isAdmin ? (b) => setState(() { _canEveryoneAddMembers = b; }) @@ -126,7 +124,7 @@ class _UpdateConversationScreen extends State { PopupMenuItem( child: Text(tr("Remove")), value: _MembersMenuChoices.REMOVE, - enabled: isOwner || + enabled: isAdmin || (_canEveryoneAddMembers && !_initialSettings.members .contains(f.id)), @@ -163,7 +161,8 @@ class _UpdateConversationScreen extends State { /// Submit the conversation Future _submitForm() async { - final settings = Conversation( + // TODO : reimplement + /* final settings = Conversation( id: isUpdating ? widget.initialSettings.id : 0, ownerID: isUpdating ? widget.initialSettings.ownerID : 0, name: _nameController.text, @@ -198,6 +197,6 @@ class _UpdateConversationScreen extends State { MainController.of(context).popPage(); if (!isUpdating) - MainController.of(context).openConversation(conversationID); + MainController.of(context).openConversation(conversationID);*/ } } diff --git a/lib/ui/tiles/conversation_message_tile.dart b/lib/ui/tiles/conversation_message_tile.dart index 8963a66..444be58 100644 --- a/lib/ui/tiles/conversation_message_tile.dart +++ b/lib/ui/tiles/conversation_message_tile.dart @@ -1,7 +1,6 @@ import 'package:comunic/models/conversation_message.dart'; import 'package:comunic/models/user.dart'; import 'package:comunic/ui/widgets/account_image_widget.dart'; -import 'package:comunic/ui/widgets/network_image_widget.dart'; import 'package:comunic/ui/widgets/text_widget.dart'; import 'package:comunic/utils/date_utils.dart'; import 'package:comunic/utils/intl_utils.dart'; @@ -72,7 +71,9 @@ class ConversationMessageTile extends StatelessWidget { /// Build widget image Widget _buildMessageImage(BuildContext context) { - return Container( + return Text(""); + // TODO : fix file + /*return Container( margin: EdgeInsets.only(bottom: 2), child: NetworkImageWidget( url: message.imageURL, @@ -80,7 +81,7 @@ class ConversationMessageTile extends StatelessWidget { width: 200, height: 200, ), - ); + );*/ } /// Build message date diff --git a/lib/ui/tiles/conversation_tile.dart b/lib/ui/tiles/conversation_tile.dart index 39c067f..ac974be 100644 --- a/lib/ui/tiles/conversation_tile.dart +++ b/lib/ui/tiles/conversation_tile.dart @@ -80,7 +80,7 @@ class ConversationTile extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.stretch, children: [ _buildSubInformation( - Icons.access_time, diffTimeFromNowToStr(conversation.lastActive)), + Icons.access_time, diffTimeFromNowToStr(conversation.lastActivity)), _buildSubInformation( Icons.group, conversation.members.length == 1 diff --git a/lib/ui/widgets/posts_list_widget.dart b/lib/ui/widgets/posts_list_widget.dart index f81619c..f82d83e 100644 --- a/lib/ui/widgets/posts_list_widget.dart +++ b/lib/ui/widgets/posts_list_widget.dart @@ -119,33 +119,36 @@ class PostsListWidgetState extends SafeState { _loading = true; - final list = !getOlder - ? await widget.getPostsList() - : await widget.getOlder(_list.oldestID); + try { + final list = !getOlder + ? await widget.getPostsList() + : await widget.getOlder(_list.oldestID); - if (list == null) return _loadError(); + if (list == null) return _loadError(); - final users = await _usersHelper.getList(list.usersID); + final users = await _usersHelper.getList(list.usersID); - if (users == null) return _loadError(); + final groups = await _groupsHelper.getList(list.groupsID); - final groups = await _groupsHelper.getList(list.groupsID); + if (groups == null) return _loadError(); - if (groups == null) return _loadError(); + if (!mounted) return; - if (!mounted) return; - - setState(() { - if (!getOlder) { - _list = list; - _users = users; - _groups = groups; - } else { - _list.addAll(list); - _users.addAll(users); - _groups.addAll(groups); - } - }); + setState(() { + if (!getOlder) { + _list = list; + _users = users; + _groups = groups; + } else { + _list.addAll(list); + _users.addAll(users); + _groups.addAll(groups); + } + }); + } catch (e, s) { + print("Failed to load post information ! $e => $s"); + _loadError(); + } _loading = false; } diff --git a/lib/ui/widgets/tablet_mode/conversations/conversation_window.dart b/lib/ui/widgets/tablet_mode/conversations/conversation_window.dart index 3ef3037..22a8528 100644 --- a/lib/ui/widgets/tablet_mode/conversations/conversation_window.dart +++ b/lib/ui/widgets/tablet_mode/conversations/conversation_window.dart @@ -81,7 +81,7 @@ class _ConversationWindowState extends SafeState { _refresh(); listen((e) { - if (e.msg.conversationID == _convID && + if (e.msg.convID == _convID && _collapsed && e.msg.userID != userID()) setState(() => _hasNewMessages = true); }); diff --git a/lib/utils/conversations_utils.dart b/lib/utils/conversations_utils.dart index 292b7f8..80a192b 100644 --- a/lib/utils/conversations_utils.dart +++ b/lib/utils/conversations_utils.dart @@ -10,16 +10,16 @@ import 'package:flutter/material.dart'; /// Open a private conversation with a given [userID] Future openPrivateConversation(BuildContext context, int userID) async { - final convID = await ConversationsHelper().getPrivate(userID); + try { + final convID = await ConversationsHelper().getPrivate(userID); - if (convID == null) { + // Open the conversation + MainController.of(context).openConversation(convID); + + return true; + } catch (e, s) { + print("Failed to find private conversation! $e => $s"); showSimpleSnack(context, tr("Could not find a private conversation!")); return false; } - - // Open the conversation - MainController.of(context).openConversation(convID); - - // Success - return true; } diff --git a/pubspec.lock b/pubspec.lock index 338efe4..acd0645 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -346,7 +346,7 @@ packages: source: hosted version: "1.8.0-nullsafety.1" path_provider: - dependency: transitive + dependency: "direct main" description: name: path_provider url: "https://pub.dartlang.org" diff --git a/pubspec.yaml b/pubspec.yaml index 92b7e03..31cd5d1 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -11,7 +11,7 @@ description: Comunic client # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. # Read more about iOS versioning at # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html -version: 1.1.1+5 +version: 1.1.2+6 environment: sdk: ">=2.1.0 <3.0.0" @@ -91,6 +91,8 @@ dependencies: # Version manager version: ^1.2.0 + path_provider: ^1.6.27 + dev_dependencies: flutter_test: sdk: flutter