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

Start conversation upgrade

This commit is contained in:
Pierre HUBERT 2021-03-10 17:54:41 +01:00
parent b094361f5a
commit dacccf57b5
35 changed files with 818 additions and 520 deletions

6
lib/constants.dart Normal file
View File

@ -0,0 +1,6 @@
/// Comunic mobile constants
///
/// @author Pierre Hubert
/// Data serialisation directory
const SERIALIZATION_DIRECTORY = "serialization";

View File

@ -1,5 +1,5 @@
import 'package:comunic/helpers/database/conversation_messages_database_helper.dart'; import 'package:comunic/helpers/serialization/conversation_message_serialization_helper.dart';
import 'package:comunic/helpers/database/conversations_database_helper.dart'; import 'package:comunic/helpers/serialization/conversations_serialization_helper.dart';
import 'package:comunic/helpers/users_helper.dart'; import 'package:comunic/helpers/users_helper.dart';
import 'package:comunic/helpers/websocket_helper.dart'; import 'package:comunic/helpers/websocket_helper.dart';
import 'package:comunic/lists/conversation_messages_list.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_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_member.dart';
import 'package:comunic/models/conversation_message.dart'; import 'package:comunic/models/conversation_message.dart';
import 'package:comunic/models/displayed_content.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_message.dart';
import 'package:comunic/models/new_conversation_settings.dart';
import 'package:comunic/models/unread_conversation.dart'; import 'package:comunic/models/unread_conversation.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';
@ -25,20 +28,16 @@ enum SendMessageResult { SUCCESS, MESSAGE_REJECTED, FAILED }
class ConversationsHelper { class ConversationsHelper {
static final _registeredConversations = Map<int, int>(); static final _registeredConversations = Map<int, int>();
final ConversationsDatabaseHelper _conversationsDatabaseHelper =
ConversationsDatabaseHelper();
final ConversationMessagesDatabaseHelper _conversationMessagesDatabaseHelper =
ConversationMessagesDatabaseHelper();
/// Create a new conversation /// Create a new conversation
/// ///
/// Return the ID of the newly created conversation or -1 in case of failure /// Return the ID of the newly created conversation or -1 in case of failure
Future<int> createConversation(Conversation settings) async { Future<int> createConversation(NewConversation settings) async {
final response = final response =
await APIRequest(uri: "conversations/create", needLogin: true, args: { await APIRequest(uri: "conversations/create", needLogin: true, args: {
"name": settings.hasName ? settings.name : "false", "name": settings.name ?? "",
"follow": settings.following ? "true" : "false", "follow": settings.follow ? "true" : "false",
"users": settings.members.join(","), "users": settings.members.join(","),
"color": settings.color ?? ","
}).addBool("canEveryoneAddMembers", settings.canEveryoneAddMembers).exec(); }).addBool("canEveryoneAddMembers", settings.canEveryoneAddMembers).exec();
if (response.code != 200) return -1; if (response.code != 200) return -1;
@ -48,72 +47,51 @@ class ConversationsHelper {
/// Update an existing conversation /// Update an existing conversation
/// ///
/// Returns a boolean depending of the success of the operation /// Throws in case of failure
Future<bool> updateConversation(Conversation settings) async { Future<void> updateConversation(NewConversationsSettings settings) async {
final request = final request = APIRequest.withLogin("conversations/updateSettings")
APIRequest(uri: "conversations/updateSettings", needLogin: true, args: { .addInt("conversationID", settings.convID)
"conversationID": settings.id.toString(), .addBool("following", settings.following);
"following": settings.following ? "true" : "false"
});
if (settings.isOwner || settings.canEveryoneAddMembers) // Update conversation settings
request.addString("members", settings.members.join(",")); if (settings.isComplete)
request
.addString("name", settings.name ?? "")
.addBool("canEveryoneAddMembers", settings.canEveryoneAddMembers)
.addString("color", settings.color ?? "");
// Update all conversation settings, if possible await request.execWithThrow();
if (settings.isOwner) {
request.addString("name", settings.hasName ? settings.name : "false");
request.addBool("canEveryoneAddMembers", settings.canEveryoneAddMembers);
}
final response = await request.exec(); // Delete old conversation entry from the database
await ConversationsSerializationHelper()
if (response.code != 200) return false; .removeElement((t) => t.id == settings.convID);
//Delete old conversation entry from the database
await _conversationsDatabaseHelper.delete(settings.id);
// Success
return true;
} }
/// Delete a conversation specified by its [id] /// Delete a conversation specified by its [id]
Future<bool> deleteConversation(int id) async { Future<void> deleteConversation(int id) async =>
final response = await APIRequest( await APIRequest.withLogin("conversations/delete")
uri: "conversations/delete", .addInt("conversationID", id)
needLogin: true, .execWithThrow();
args: {
"conversationID": id.toString(),
},
).exec();
return response.code == 200;
}
/// Download the list of conversations from the server /// Download the list of conversations from the server
///
/// Throws an exception in case of failure
Future<ConversationsList> downloadList() async { Future<ConversationsList> downloadList() async {
final response = final response =
await APIRequest(uri: "conversations/getList", needLogin: true).exec(); await APIRequest.withLogin("conversations/getList").execWithThrow();
if (response.code != 200) return null;
try {
ConversationsList list = ConversationsList(); ConversationsList list = ConversationsList();
response.getArray().forEach((f) => list.add(apiToConversation(f))); response.getArray().forEach((f) => list.add(apiToConversation(f)));
// Update the database // Update the database
await _conversationsDatabaseHelper.clearTable(); await ConversationsSerializationHelper().setList(list);
await _conversationsDatabaseHelper.insertAll(list);
return list; return list;
} on Exception catch (e) {
print(e.toString());
return null;
}
} }
/// Get the local list of conversations /// Get the local list of conversations
Future<ConversationsList> getCachedList() async { Future<ConversationsList> getCachedList() async {
final list = await _conversationsDatabaseHelper.getAll(); final list = await ConversationsSerializationHelper().getList();
list.sort(); list.sort();
return list; return list;
} }
@ -124,41 +102,31 @@ class ConversationsHelper {
final response = await APIRequest( final response = await APIRequest(
uri: "conversations/getInfoOne", uri: "conversations/getInfoOne",
needLogin: true, needLogin: true,
args: {"conversationID": id.toString()}).exec(); args: {"conversationID": id.toString()}).execWithThrow();
if (response.code != 200) return null;
final conversation = apiToConversation(response.getObject()); final conversation = apiToConversation(response.getObject());
_conversationsDatabaseHelper.insertOrUpdate(conversation);
await ConversationsSerializationHelper()
.insertOrReplaceElement((c) => c.id == conversation.id, conversation);
return conversation; return conversation;
} on Exception catch (e) { } on Exception catch (e, s) {
print(e.toString()); print("Could not get information about a single conversation ! $e => $s");
print("Could not get information about a single conversation !");
return null; 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 /// cached version of the conversation will be used, else it will always get
/// the information from the server /// the information from the server. The method throws an [Exception] in
Future<Conversation> 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
/// case of failure /// case of failure
/// ///
/// Return value of this method is never null. /// Return value of this method is never null.
Future<Conversation> getSingleOrThrow(int id, {bool force = false}) async { Future<Conversation> getSingle(int id, {bool force = false}) async {
final conv = await this.getSingle(id, force: force); if (force ||
!await ConversationsSerializationHelper().any((c) => c.id == id))
if (conv == null) return await _downloadSingle(id);
throw Exception("Could not get information about the conversation!"); else
return await ConversationsSerializationHelper().get(id);
return conv;
} }
/// Get the name of a [conversation]. This requires information /// Get the name of a [conversation]. This requires information
@ -170,9 +138,9 @@ class ConversationsHelper {
String name = ""; String name = "";
int count = 0; int count = 0;
for (int i = 0; i < 3 && i < conversation.members.length; i++) 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 ? ", " : "") + name += (count > 0 ? ", " : "") +
users.getUser(conversation.members[i]).fullName; users.getUser(conversation.members[i].userID).fullName;
count++; count++;
} }
@ -184,6 +152,8 @@ class ConversationsHelper {
/// Search and return a private conversation with a given [userID]. If such /// Search and return a private conversation with a given [userID]. If such
/// conversation does not exists, it is created if [allowCreate] is set to /// conversation does not exists, it is created if [allowCreate] is set to
/// true /// true
///
/// Throws an exception in case of failure
Future<int> getPrivate(int userID, {bool allowCreate = true}) async { Future<int> getPrivate(int userID, {bool allowCreate = true}) async {
final response = await APIRequest( final response = await APIRequest(
uri: "conversations/getPrivate", uri: "conversations/getPrivate",
@ -192,17 +162,10 @@ class ConversationsHelper {
"otherUser": userID.toString(), "otherUser": userID.toString(),
"allowCreate": allowCreate.toString() "allowCreate": allowCreate.toString()
}, },
).exec(); ).execWithThrow();
if (response.code != 200) return null;
// Get and return conversation ID // Get and return conversation ID
try {
return int.parse(response.getObject()["conversationsID"][0].toString()); return int.parse(response.getObject()["conversationsID"][0].toString());
} catch (e) {
e.toString();
return null;
}
} }
/// Asynchronously get the name of the conversation /// Asynchronously get the name of the conversation
@ -210,15 +173,13 @@ class ConversationsHelper {
/// Unlike the synchronous method, this method does not need information /// Unlike the synchronous method, this method does not need information
/// about the members of the conversation /// about the members of the conversation
/// ///
/// Returns null in case of failure /// Throws an exception in case of failure
static Future<String> getConversationNameAsync( static Future<String> getConversationNameAsync(
Conversation conversation) async { Conversation conversation) async {
if (conversation.hasName) return conversation.name; if (conversation.hasName) return conversation.name;
//Get information about the members of the conversation //Get information about the members of the conversation
final members = await UsersHelper().getUsersInfo(conversation.members); final members = await UsersHelper().getList(conversation.membersID);
if (members == null) return null;
return ConversationsHelper.getConversationName(conversation, members); return ConversationsHelper.getConversationName(conversation, members);
} }
@ -226,14 +187,18 @@ class ConversationsHelper {
/// Turn an API entry into a [Conversation] object /// Turn an API entry into a [Conversation] object
static Conversation apiToConversation(Map<String, dynamic> map) { static Conversation apiToConversation(Map<String, dynamic> map) {
return Conversation( return Conversation(
id: map["ID"], id: map["id"],
ownerID: map["ID_owner"], lastActivity: map["last_activity"],
lastActive: map["last_active"], name: map["name"],
name: map["name"] == false ? null : map["name"], color: map["color"],
following: map["following"] == 1, logoURL: map["logo"],
sawLastMessage: map["saw_last_message"] == 1, groupID: map["group_id"],
members: List<int>.from(map["members"]), members: map["members"]
canEveryoneAddMembers: map["canEveryoneAddMembers"], .cast<Map<String, dynamic>>()
.map(apiToConversationMember)
.toList()
.cast<ConversationMember>(),
canEveryoneAddMembers: map["can_everyone_add_members"],
callCapabilities: map["can_have_video_call"] callCapabilities: map["can_have_video_call"]
? CallCapabilities.VIDEO ? CallCapabilities.VIDEO
: (map["can_have_call"] : (map["can_have_call"]
@ -242,10 +207,21 @@ class ConversationsHelper {
isHavingCall: map["has_call_now"]); isHavingCall: map["has_call_now"]);
} }
static ConversationMember apiToConversationMember(Map<String, dynamic> 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 /// Parse a list of messages given by the server
///
/// Throws an exception in case of failure
Future<ConversationMessagesList> _parseConversationMessageFromServer( Future<ConversationMessagesList> _parseConversationMessageFromServer(
int conversationID, APIResponse response) async { int conversationID, APIResponse response) async {
if (response.code != 200) return null; response.assertOk();
// Parse the response of the server // Parse the response of the server
ConversationMessagesList list = ConversationMessagesList(); ConversationMessagesList list = ConversationMessagesList();
@ -256,7 +232,8 @@ class ConversationsHelper {
}); });
// Save messages in the cache // Save messages in the cache
_conversationMessagesDatabaseHelper.insertOrUpdateAll(list); await ConversationsMessagesSerializationHelper(conversationID)
.insertOrReplaceAll(list);
return list; return list;
} }
@ -265,6 +242,8 @@ class ConversationsHelper {
/// ///
/// Set [lastMessageID] to 0 to specify that we do not have any message of the /// Set [lastMessageID] to 0 to specify that we do not have any message of the
/// conversation yet or another value else /// conversation yet or another value else
///
/// Throws an exception in case of failure
Future<ConversationMessagesList> _downloadNewMessagesSingle( Future<ConversationMessagesList> _downloadNewMessagesSingle(
int conversationID, int conversationID,
{int lastMessageID = 0}) async { {int lastMessageID = 0}) async {
@ -275,26 +254,26 @@ class ConversationsHelper {
args: { args: {
"conversationID": conversationID.toString(), "conversationID": conversationID.toString(),
"last_message_id": lastMessageID.toString() "last_message_id": lastMessageID.toString()
}).exec(); }).execWithThrow();
return await _parseConversationMessageFromServer(conversationID, response); return await _parseConversationMessageFromServer(conversationID, response);
} }
/// Get older messages for a given conversation from an online source /// Get older messages for a given conversation from an online source
///
/// Throws in case of failure
Future<ConversationMessagesList> getOlderMessages({ Future<ConversationMessagesList> getOlderMessages({
@required int conversationID, @required int conversationID,
@required int oldestMessagesID, @required int oldestMessagesID,
int limit = 15, int limit = 15,
}) async { }) async {
// Perform the request online // Perform the request online
final response = await APIRequest( final response =
uri: "conversations/get_older_messages", await APIRequest.withLogin("conversations/get_older_messages", args: {
needLogin: true,
args: {
"conversationID": conversationID.toString(), "conversationID": conversationID.toString(),
"oldest_message_id": oldestMessagesID.toString(), "oldest_message_id": oldestMessagesID.toString(),
"limit": limit.toString() "limit": limit.toString()
}).exec(); }).execWithThrow();
return await _parseConversationMessageFromServer(conversationID, response); return await _parseConversationMessageFromServer(conversationID, response);
} }
@ -304,6 +283,8 @@ class ConversationsHelper {
/// If [lastMessageID] is set to 0 then we retrieve the last messages of /// If [lastMessageID] is set to 0 then we retrieve the last messages of
/// the conversation. /// the conversation.
/// Otherwise [lastMessageID] contains the ID of the last known message /// Otherwise [lastMessageID] contains the ID of the last known message
///
/// Throws in case of failure
Future<ConversationMessagesList> getNewMessages( Future<ConversationMessagesList> getNewMessages(
{@required int conversationID, {@required int conversationID,
int lastMessageID = 0, int lastMessageID = 0,
@ -312,16 +293,8 @@ class ConversationsHelper {
return await _downloadNewMessagesSingle(conversationID, return await _downloadNewMessagesSingle(conversationID,
lastMessageID: lastMessageID); lastMessageID: lastMessageID);
else else
return await _conversationMessagesDatabaseHelper return await ConversationsMessagesSerializationHelper(conversationID)
.getAllMessagesConversations(conversationID, .getList();
lastMessageID: lastMessageID);
}
/// Get a single conversation message from the local database
///
/// Returns the message if found or null in case of failure
Future<ConversationMessage> getSingleMessageFromCache(int messageID) async {
return await _conversationMessagesDatabaseHelper.get(messageID);
} }
/// Send a new message to the server /// Send a new message to the server
@ -353,14 +326,13 @@ class ConversationsHelper {
} }
/// Save / Update a message into the database /// Save / Update a message into the database
Future<void> saveMessage(ConversationMessage msg) async { Future<void> saveMessage(ConversationMessage msg) async =>
await _conversationMessagesDatabaseHelper.insertOrUpdate(msg); await ConversationsMessagesSerializationHelper(msg.convID)
} .insertOrReplace(msg);
/// Remove a message from the database /// Remove a message from the database
Future<void> removeMessage(int msgID) async { Future<void> removeMessage(ConversationMessage msg) async =>
await _conversationMessagesDatabaseHelper.delete(msgID); await ConversationsMessagesSerializationHelper(msg.convID).remove(msg);
}
/// Update a message content /// Update a message content
Future<bool> updateMessage(int id, String newContent) async { Future<bool> updateMessage(int id, String newContent) async {
@ -397,11 +369,8 @@ class ConversationsHelper {
return UnreadConversationsList() return UnreadConversationsList()
..addAll(list.map((f) => UnreadConversation( ..addAll(list.map((f) => UnreadConversation(
id: f["id"], conv: apiToConversation(f["conv"]),
convName: f["conv_name"], message: apiToConversationMessage(f["message"]),
lastActive: f["last_active"],
userID: f["userID"],
message: f["message"],
))); )));
} }
@ -432,13 +401,38 @@ class ConversationsHelper {
static ConversationMessage apiToConversationMessage( static ConversationMessage apiToConversationMessage(
Map<String, dynamic> map, Map<String, dynamic> map,
) { ) {
return ConversationMessage( var file;
id: map["ID"], if (map["file"] != null) {
conversationID: map["convID"], final fileMap = map["file"];
userID: map["ID_user"], file = ConversationMessageFile(
timeInsert: map["time_insert"], url: fileMap["url"],
message: DisplayedString(map["message"]), size: fileMap["size"],
imageURL: map["image_path"], 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"],
convID: map["conv_id"],
userID: map["user_id"],
timeSent: map["time_sent"],
message: DisplayedString(map["message"] ?? ""),
file: file,
serverMessage: serverMessage);
}
} }

View File

@ -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<ConversationMessage> {
@override
ConversationMessage initializeFromMap(Map<String, dynamic> map) {
return ConversationMessage.fromMap(map);
}
@override
String tableName() {
return ConversationsMessagesTableContract.TABLE_NAME;
}
/// Get all the message cached for a given conversation
Future<ConversationMessagesList> 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;
}
}

View File

@ -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<Conversation> {
@override
Conversation initializeFromMap(Map<String, dynamic> map) {
return Conversation.fromMap(map);
}
@override
String tableName() {
return ConversationTableContract.TABLE_NAME;
}
@override
Future<ConversationsList> getAll() async {
ConversationsList list = ConversationsList();
list.addAll(await super.getAll());
return list;
}
}

View File

@ -25,30 +25,6 @@ abstract class UserTableContract {
static const C_CUSTOM_EMOJIES = "custom_emojies"; 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 /// Friends table contract
abstract class FriendsListTableContract { abstract class FriendsListTableContract {
static const TABLE_NAME = "friends"; static const TABLE_NAME = "friends";

View File

@ -45,14 +45,6 @@ abstract class DatabaseHelper {
// Drop users table // Drop users table
await db.execute("DROP TABLE IF EXISTS ${UserTableContract.TABLE_NAME}"); 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 // Drop friends list table
await db await db
.execute("DROP TABLE IF EXISTS ${FriendsListTableContract.TABLE_NAME}"); .execute("DROP TABLE IF EXISTS ${FriendsListTableContract.TABLE_NAME}");
@ -74,29 +66,6 @@ abstract class DatabaseHelper {
"${UserTableContract.C_CUSTOM_EMOJIES} TEXT" "${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 // Friends list table
await db.execute("CREATE TABLE ${FriendsListTableContract.TABLE_NAME} (" await db.execute("CREATE TABLE ${FriendsListTableContract.TABLE_NAME} ("
"${FriendsListTableContract.C_ID} INTEGER PRIMARY KEY, " "${FriendsListTableContract.C_ID} INTEGER PRIMARY KEY, "

View File

@ -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<T> extends Comparable<T> {
Map<String, dynamic> toJson();
}
abstract class BaseSerializationHelper<T extends SerializableElement> {
/// List cache
List<T> _cache;
/// The name of the type of data to serialise
String get type;
/// Parse an json entry into a [T] object
T parse(Map<String, dynamic> m);
/// Get the file where data should be stored
Future<File> _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<void> _loadCache() async {
if (_cache != null) return;
try {
final file = await _getFilePath();
if (!await file.exists()) return _cache = List();
final List<dynamic> json = jsonDecode(await file.readAsString());
_cache = json.cast<Map<String, dynamic>>().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<void> _saveCache() async {
final file = await _getFilePath();
await file.writeAsString(jsonEncode(
_cache.map((e) => e.toJson()).toList().cast<Map<String, dynamic>>()));
}
/// Get the current list of elements
Future<List<T>> getList() async {
await _loadCache();
return List.from(_cache);
}
/// Set a new list of conversations
Future<void> setList(List<T> list) async {
_cache = List.from(list);
await _saveCache();
}
/// Insert new element
Future<void> insert(T el) async {
await _loadCache();
_cache.add(el);
_cache.sort();
await _saveCache();
}
/// Insert new element
Future<void> insertMany(List<T> els) async {
await _loadCache();
_cache.addAll(els);
_cache.sort();
await _saveCache();
}
/// Check if any entry in the last match the predicate
Future<bool> 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<T> first(bool filter(T t)) async {
await _loadCache();
return _cache.firstWhere((element) => filter(element));
}
/// Replace an element with another one
Future<void> 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<void> removeElement(bool isToRemove(T t)) async {
await _loadCache();
_cache.removeWhere((element) => isToRemove(element));
await _saveCache();
}
}

View File

@ -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<int, ConversationsMessagesSerializationHelper> _instances =
HashSet() as HashMap<int, ConversationsMessagesSerializationHelper>;
class ConversationsMessagesSerializationHelper
extends BaseSerializationHelper<ConversationMessage> {
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<String, dynamic> m) =>
ConversationMessage.fromJson(m);
@override
String get type => "conv-messages-$convID";
Future<ConversationMessagesList> getList() async =>
ConversationMessagesList()..addAll(await super.getList());
Future<void> insertOrReplace(ConversationMessage msg) async =>
await insertOrReplaceElement((t) => t.id == msg.id, msg);
Future<void> remove(ConversationMessage msg) async =>
await removeElement((t) => t.id == msg.id);
/// Insert or replace a list of messages
Future<void> insertOrReplaceAll(List<ConversationMessage> list) async {
for (var message in list)
await insertOrReplaceElement((t) => t.id == message.id, message);
}
}

View File

@ -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<Conversation> {
/// Singleton
factory ConversationsSerializationHelper() {
if (_cache == null) _cache = ConversationsSerializationHelper._();
return _cache;
}
ConversationsSerializationHelper._();
@override
Conversation parse(Map<String, dynamic> m) => Conversation.fromJson(m);
@override
String get type => "conversations";
Future<ConversationsList> getList() async =>
ConversationsList()..addAll(await super.getList());
/// Get a conversation
Future<Conversation> get(int id) => first((t) => t.id == id);
}

View File

@ -91,9 +91,16 @@ class UsersHelper {
} }
/// Get users information from a given [Set] /// Get users information from a given [Set]
///
/// Throws in case of failure
Future<UsersList> getList(Set<int> users, Future<UsersList> getList(Set<int> users,
{bool forceDownload = false}) async { {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 /// Get users information

View File

@ -8,11 +8,11 @@ import 'package:comunic/models/conversation.dart';
/// @author Pierre HUBERT /// @author Pierre HUBERT
class ConversationsList extends ListBase<Conversation> { class ConversationsList extends ListBase<Conversation> {
final List<Conversation> _list = List(); final List<Conversation> _list = List();
UsersList users; UsersList users;
set length(l) => _list.length = l; set length(l) => _list.length = l;
int get length => _list.length; int get length => _list.length;
@override @override
@ -22,12 +22,9 @@ class ConversationsList extends ListBase<Conversation> {
void operator []=(int index, Conversation value) => _list[index] = value; void operator []=(int index, Conversation value) => _list[index] = value;
/// Get the entire lists of users ID in this list /// Get the entire lists of users ID in this list
List<int> get allUsersID { Set<int> get allUsersID {
final List<int> list = List(); final Set<int> list = Set();
forEach((c) => c.members.forEach((id){ forEach((c) => c.members.forEach((member) => list.add(member.userID)));
if(!list.contains(id))
list.add(id);
}));
return list; return list;
} }
} }

View File

@ -18,7 +18,7 @@ class MembershipList extends AbstractList<Membership> {
case MembershipType.GROUP: case MembershipType.GROUP:
break; break;
case MembershipType.CONVERSATION: case MembershipType.CONVERSATION:
s.addAll(m.conversation.members); s.addAll(m.conversation.membersID);
break; break;
} }
}); });

View File

@ -7,5 +7,9 @@ import 'package:comunic/models/unread_conversation.dart';
class UnreadConversationsList extends AbstractList<UnreadConversation> { class UnreadConversationsList extends AbstractList<UnreadConversation> {
/// Get the ID of the users included in this list /// Get the ID of the users included in this list
Set<int> get usersID => new Set<int>()..addAll(map((f) => f.userID)); Set<int> get usersID {
final set = Set();
forEach((element) => set.addAll(element.message.usersID));
return set;
}
} }

View File

@ -1,7 +1,6 @@
import 'package:comunic/helpers/database/database_contract.dart'; import 'package:comunic/helpers/serialization/base_serialization_helper.dart';
import 'package:comunic/models/cache_model.dart'; import 'package:comunic/models/conversation_member.dart';
import 'package:comunic/utils/account_utils.dart'; import 'package:comunic/utils/account_utils.dart';
import 'package:comunic/utils/list_utils.dart';
import 'package:meta/meta.dart'; import 'package:meta/meta.dart';
/// Conversation model /// Conversation model
@ -10,79 +9,86 @@ import 'package:meta/meta.dart';
enum CallCapabilities { NONE, AUDIO, VIDEO } enum CallCapabilities { NONE, AUDIO, VIDEO }
class Conversation extends CacheModel implements Comparable { class Conversation extends SerializableElement<Conversation> {
final int ownerID; final int id;
final int lastActive; final int lastActivity;
final String name; final String name;
final bool following; final String color;
final bool sawLastMessage; final String logoURL;
final List<int> members; final int groupID;
final List<ConversationMember> members;
final bool canEveryoneAddMembers; final bool canEveryoneAddMembers;
final CallCapabilities callCapabilities; final CallCapabilities callCapabilities;
final bool isHavingCall; final bool isHavingCall;
const Conversation({ Conversation({
@required int id, @required this.id,
@required this.ownerID, @required this.lastActivity,
@required this.lastActive,
@required this.name, @required this.name,
@required this.following, @required this.color,
@required this.sawLastMessage, @required this.logoURL,
@required this.groupID,
@required this.members, @required this.members,
@required this.canEveryoneAddMembers, @required this.canEveryoneAddMembers,
this.callCapabilities = CallCapabilities.NONE, this.callCapabilities = CallCapabilities.NONE,
this.isHavingCall = false, this.isHavingCall = false,
}) : assert(id != null), }) : assert(id != null),
assert(ownerID != null), assert(lastActivity != null),
assert(lastActive != null),
assert(following != null),
assert(sawLastMessage != null),
assert(members != null), assert(members != null),
assert(canEveryoneAddMembers != null), assert(canEveryoneAddMembers != null),
assert(callCapabilities != null), assert(callCapabilities != null),
assert(isHavingCall != null), assert(isHavingCall != null);
super(id: id);
/// Check out whether a conversation has a fixed name or not /// Check out whether a conversation has a fixed name or not
bool get hasName => this.name != null; bool get hasName => this.name != null;
/// Check out whether current user of the application is the owner of it or /// Get current user membership
/// not ConversationMember get membership =>
bool get isOwner => this.ownerID == userID(); members.firstWhere((m) => m.userID == userID());
Conversation.fromMap(Map<String, dynamic> map) /// Check out whether current user of the application is an admin
: ownerID = map[ConversationTableContract.C_OWNER_ID], bool get isAdmin => membership.isAdmin;
lastActive = map[ConversationTableContract.C_LAST_ACTIVE],
name = map[ConversationTableContract.C_NAME], /// Check it current user is following the conversation or not
following = map[ConversationTableContract.C_FOLLOWING] == 1, bool get following => membership.following;
sawLastMessage = map[ConversationTableContract.C_SAW_LAST_MESSAGE] == 1,
members = /// Get the list of members in the conversation
listToIntList(map[ConversationTableContract.C_MEMBERS].split(",")), Set<int> get membersID => members.map((e) => e.userID).toSet();
canEveryoneAddMembers =
map[ConversationTableContract.C_CAN_EVERYONE_ADD_MEMBERS] == 1, /// Check if the last message has been seen or not
bool get sawLastMessage => lastActivity <= membership.lastAccessTime;
Conversation.fromJson(Map<String, dynamic> 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 // By default, we can not do any call
callCapabilities = CallCapabilities.NONE, callCapabilities = CallCapabilities.NONE,
isHavingCall = false, isHavingCall = false;
super.fromMap(map);
@override Map<String, dynamic> toJson() {
Map<String, dynamic> toMap() {
return { return {
ConversationTableContract.C_ID: id, "id": id,
ConversationTableContract.C_OWNER_ID: ownerID, "name": name,
ConversationTableContract.C_LAST_ACTIVE: lastActive, "color": color,
ConversationTableContract.C_NAME: name, "logoURL": logoURL,
ConversationTableContract.C_FOLLOWING: following ? 1 : 0, "groupID": groupID,
ConversationTableContract.C_SAW_LAST_MESSAGE: sawLastMessage ? 1 : 0, "lastActivity": lastActivity,
ConversationTableContract.C_MEMBERS: members.join(","), "members": members.map((e) => e.toJson()).toList(),
ConversationTableContract.C_CAN_EVERYONE_ADD_MEMBERS: "canEveryoneAddMembers": canEveryoneAddMembers,
canEveryoneAddMembers ? 1 : 0
}; };
} }
@override @override
int compareTo(other) { int compareTo(Conversation other) {
return other.lastActive.compareTo(this.lastActive); return other.lastActivity.compareTo(this.lastActivity);
} }
} }

View File

@ -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<String, dynamic> toJson() => {
'userID': userID,
'lastMessageSeen': lastMessageSeen,
'lastAccessTime': lastAccessTime,
'following': following,
'isAdmin': isAdmin,
};
ConversationMember.fromJSON(Map<String, dynamic> json)
: userID = json["userID"],
lastMessageSeen = json["lastMessageSeen"],
lastAccessTime = json["lastAccessTime"],
following = json["following"],
isAdmin = json["isAdmin"];
}

View File

@ -1,5 +1,4 @@
import 'package:comunic/helpers/database/database_contract.dart'; import 'package:comunic/helpers/serialization/base_serialization_helper.dart';
import 'package:comunic/models/cache_model.dart';
import 'package:comunic/models/displayed_content.dart'; import 'package:comunic/models/displayed_content.dart';
import 'package:comunic/utils/account_utils.dart' as account; import 'package:comunic/utils/account_utils.dart' as account;
import 'package:meta/meta.dart'; import 'package:meta/meta.dart';
@ -8,59 +7,171 @@ import 'package:meta/meta.dart';
/// ///
/// @author Pierre HUBERT /// @author Pierre HUBERT
class ConversationMessage extends CacheModel implements Comparable { class ConversationMessageFile {
final int id; final String url;
final int conversationID; 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<String, dynamic> toJson() => {
"url": url,
"size": size,
"name": name,
"thumbnail": thumbnail,
"type": type
};
ConversationMessageFile.fromJson(Map<String, dynamic> 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 userID;
final int timeInsert; final int userWhoAdded;
final DisplayedString message; final int userAdded;
final String imageURL; final int userWhoRemoved;
final int userRemoved;
const ConversationMessage({ const ConversationServerMessage({
@required this.id, @required this.type,
@required this.conversationID,
@required this.userID, @required this.userID,
@required this.timeInsert, @required this.userWhoAdded,
@required this.message, @required this.userAdded,
@required this.imageURL, @required this.userWhoRemoved,
}) : assert(id != null), @required this.userRemoved,
assert(userID != null), }) : assert(type != null),
assert(timeInsert != null), assert(userID != null ||
assert(message != null), (type != ConversationServerMessageType.USER_CREATED_CONVERSATION &&
super(id: id); 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<int> 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<String, dynamic> toJson() => {
"type": type.toString(),
"userID": userID,
"userWhoAdded": userWhoAdded,
"userAdded": userAdded,
"userWhoRemoved": userWhoRemoved,
"userRemoved": userRemoved,
};
ConversationServerMessage.fromJson(Map<String, dynamic> 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<ConversationMessage> {
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 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; bool get isOwner => account.userID() == userID;
/// Get the list of the ID of the users implied in this message
Set<int> get usersID {
if (userID != null) return Set()..add(userID);
return serverMessage.usersID;
}
@override @override
int compareTo(other) { int compareTo(ConversationMessage other) {
return id.compareTo(other.id); return id.compareTo(other.id);
} }
@override Map<String, dynamic> toJson() {
Map<String, dynamic> toMap() {
return { return {
ConversationsMessagesTableContract.C_ID: id, "id": id,
ConversationsMessagesTableContract.C_CONVERSATION_ID: conversationID, "convID": convID,
ConversationsMessagesTableContract.C_USER_ID: userID, "userID": userID,
ConversationsMessagesTableContract.C_TIME_INSERT: timeInsert, "timeSent": timeSent,
ConversationsMessagesTableContract.C_MESSAGE: message.content, "message": message,
ConversationsMessagesTableContract.C_IMAGE_URL: imageURL "file": file?.toJson(),
"serverMessage": serverMessage?.toJson(),
}; };
} }
ConversationMessage.fromMap(Map<String, dynamic> map) ConversationMessage.fromJson(Map<String, dynamic> map)
: id = map[ConversationsMessagesTableContract.C_ID], : id = map["id"],
conversationID = convID = map["convID"],
map[ConversationsMessagesTableContract.C_CONVERSATION_ID], userID = map["userID"],
userID = map[ConversationsMessagesTableContract.C_USER_ID], timeSent = map["timeSent"],
timeInsert = map[ConversationsMessagesTableContract.C_TIME_INSERT], message = DisplayedString(map["message"]),
message = DisplayedString(map[ConversationsMessagesTableContract.C_MESSAGE]), file = map["file"],
imageURL = map[ConversationsMessagesTableContract.C_IMAGE_URL], serverMessage = map["serverMessage"];
super.fromMap(map);
} }

View File

@ -43,7 +43,7 @@ class Membership {
case MembershipType.GROUP: case MembershipType.GROUP:
return groupLastActive; return groupLastActive;
case MembershipType.CONVERSATION: case MembershipType.CONVERSATION:
return conversation.lastActive; return conversation.lastActivity;
default: default:
throw Exception("Unreachable statment!"); throw Exception("Unreachable statment!");
} }

View File

@ -0,0 +1,24 @@
import 'package:flutter/cupertino.dart';
/// New conversation information
///
/// @author Pierre Hubert
class NewConversation {
final String name;
final List<int> 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);
}

View File

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

View File

@ -1,3 +1,5 @@
import 'package:comunic/models/conversation.dart';
import 'package:comunic/models/conversation_message.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
/// Unread conversation information /// Unread conversation information
@ -5,21 +7,12 @@ import 'package:flutter/material.dart';
/// @author Pierre Hubert /// @author Pierre Hubert
class UnreadConversation { class UnreadConversation {
final int id; final Conversation conv;
final String convName; final ConversationMessage message;
final int lastActive;
final int userID;
final String message;
const UnreadConversation({ const UnreadConversation({
@required this.id, @required this.conv,
@required this.convName,
@required this.lastActive,
@required this.userID,
@required this.message, @required this.message,
}) : assert(id != null), }) : assert(conv != null),
assert(convName != null),
assert(lastActive != null),
assert(userID != null),
assert(message != null); assert(message != null);
} }

View File

@ -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/routes/update_conversation_route.dart';
import 'package:comunic/ui/screens/conversation_screen.dart'; import 'package:comunic/ui/screens/conversation_screen.dart';
import 'package:comunic/ui/widgets/comunic_back_button_widget.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/intl_utils.dart';
import 'package:comunic/utils/ui_utils.dart'; import 'package:comunic/utils/ui_utils.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -25,7 +26,7 @@ class ConversationRoute extends StatefulWidget {
State<StatefulWidget> createState() => _ConversationRouteState(); State<StatefulWidget> createState() => _ConversationRouteState();
} }
class _ConversationRouteState extends State<ConversationRoute> { class _ConversationRouteState extends SafeState<ConversationRoute> {
final ConversationsHelper _conversationsHelper = ConversationsHelper(); final ConversationsHelper _conversationsHelper = ConversationsHelper();
Conversation _conversation; Conversation _conversation;
String _conversationName; String _conversationName;
@ -42,8 +43,9 @@ class _ConversationRouteState extends State<ConversationRoute> {
Future<void> _loadConversation() async { Future<void> _loadConversation() async {
setError(false); setError(false);
_conversation = await _conversationsHelper.getSingle(widget.conversationID, try {
force: true); _conversation = await _conversationsHelper
.getSingle(widget.conversationID, force: true);
if (_conversation == null) return setError(true); if (_conversation == null) return setError(true);
@ -52,11 +54,11 @@ class _ConversationRouteState extends State<ConversationRoute> {
if (!this.mounted) return null; if (!this.mounted) return null;
if (conversationName == null) return setError(true); setState(() => _conversationName = conversationName);
} catch (e, s) {
setState(() { print("Failed to get conversation name! $e => $s");
_conversationName = conversationName; setError(true);
}); }
} }
void _openSettings() { void _openSettings() {

View File

@ -42,19 +42,18 @@ class _UpdateConversationRoute extends State<UpdateConversationRoute> {
Future<void> _loadConversation() async { Future<void> _loadConversation() async {
setError(false); setError(false);
try {
final conversation = await ConversationsHelper() final conversation = await ConversationsHelper()
.getSingle(widget.conversationID, force: true); .getSingle(widget.conversationID, force: true);
if (conversation == null) return setError(true);
//Load information about the members of the conversation //Load information about the members of the conversation
_membersInfo = await UsersHelper().getUsersInfo(conversation.members); _membersInfo = await UsersHelper().getList(conversation.membersID);
if (_membersInfo == null) return setError(true); setState(() => _conversation = conversation);
} catch (e, s) {
setState(() { print("Failed to load conversation information! $e=>$s");
_conversation = conversation; setError(true);
}); }
} }
/// Build the body of this widget /// Build the body of this widget

View File

@ -110,7 +110,7 @@ class _CallScreenState extends SafeState<CallScreen> {
// First, load information about the conversation // First, load information about the conversation
_conversation = _conversation =
await ConversationsHelper().getSingleOrThrow(convID, force: true); await ConversationsHelper().getSingle(convID, force: true);
_convName = _convName =
await ConversationsHelper.getConversationNameAsync(_conversation); await ConversationsHelper.getConversationNameAsync(_conversation);
assert(_convName != null); assert(_convName != null);

View File

@ -32,8 +32,7 @@ class _ConversationMembersScreenState extends State<ConversationMembersScreen> {
Future<void> _refresh() async { Future<void> _refresh() async {
_conversation = _conversation =
await ConversationsHelper().getSingle(widget.convID, force: true); await ConversationsHelper().getSingle(widget.convID, force: true);
_members = _members = await UsersHelper().getListWithThrow(_conversation.membersID);
await UsersHelper().getListWithThrow(_conversation.members.toSet());
} }
@override @override
@ -55,12 +54,12 @@ class _ConversationMembersScreenState extends State<ConversationMembersScreen> {
); );
Widget _buildItem(BuildContext context, int index) { 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( return ListTile(
leading: AccountImageWidget(user: user), leading: AccountImageWidget(user: user),
title: Text(user.displayName), title: Text(user.displayName),
subtitle: subtitle: Text(member.isAdmin ? tr("Admin") : tr("Member")),
Text(_conversation.ownerID == user.id ? tr("Owner") : tr("Member")),
); );
} }
} }

View File

@ -70,9 +70,8 @@ class _ConversationScreenState extends SafeState<ConversationScreen> {
}); });
/// Method called when an error occurred while loading messages /// Method called when an error occurred while loading messages
void _errorLoading() { void _errorLoading() =>
_setError(_messages == null ? ErrorLevel.MAJOR : ErrorLevel.MINOR); _setError(_messages == null ? ErrorLevel.MAJOR : ErrorLevel.MINOR);
}
/// Load the first conversations /// Load the first conversations
Future<void> _init() async { Future<void> _init() async {
@ -86,22 +85,27 @@ class _ConversationScreenState extends SafeState<ConversationScreen> {
.registerConversationEvents(widget.conversationID); .registerConversationEvents(widget.conversationID);
this.listen<NewConversationMessageEvent>((ev) async { this.listen<NewConversationMessageEvent>((ev) async {
if (ev.msg.conversationID == widget.conversationID) { if (ev.msg.convID == widget.conversationID) {
try {
await _conversationsHelper.saveMessage(ev.msg); await _conversationsHelper.saveMessage(ev.msg);
await _applyNewMessages(ConversationMessagesList()..add(ev.msg)); await _applyNewMessages(ConversationMessagesList()..add(ev.msg));
} catch (e, s) {
print("Failed to show new message! $e => $s");
_errorLoading();
}
} }
}); });
this.listen<UpdatedConversationMessageEvent>((ev) async { this.listen<UpdatedConversationMessageEvent>((ev) async {
if (ev.msg.conversationID == widget.conversationID) { if (ev.msg.convID == widget.conversationID) {
await _conversationsHelper.saveMessage(ev.msg); await _conversationsHelper.saveMessage(ev.msg);
setState(() => _messages.replace(ev.msg)); setState(() => _messages.replace(ev.msg));
} }
}); });
this.listen<DeletedConversationMessageEvent>((ev) async { this.listen<DeletedConversationMessageEvent>((ev) async {
if (ev.msg.conversationID == widget.conversationID) { if (ev.msg.convID == widget.conversationID) {
await _conversationsHelper.removeMessage(ev.msg.id); await _conversationsHelper.removeMessage(ev.msg);
setState(() => _messages.removeMsg(ev.msg.id)); setState(() => _messages.removeMsg(ev.msg.id));
} }
}); });
@ -116,19 +120,23 @@ class _ConversationScreenState extends SafeState<ConversationScreen> {
Future<void> _loadMessages(bool online) async { Future<void> _loadMessages(bool online) async {
if (!mounted) return; if (!mounted) return;
try {
//First, get the messages //First, get the messages
final messages = await _conversationsHelper.getNewMessages( final messages = await _conversationsHelper.getNewMessages(
conversationID: widget.conversationID, conversationID: widget.conversationID,
lastMessageID: _messages == null ? 0 : _messages.lastMessageID, 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 // In case we are offline and we did not get any message we do not do
// anything (we wait for the online request) // anything (we wait for the online request)
if (messages.length == 0 && !online) return; 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 /// Get older messages
@ -136,7 +144,7 @@ class _ConversationScreenState extends SafeState<ConversationScreen> {
if (_loadingOlderMessages != _OlderMessagesLevel.NONE || if (_loadingOlderMessages != _OlderMessagesLevel.NONE ||
_messages == null || _messages == null ||
_messages.length == 0) return; _messages.length == 0) return;
try {
// Let's start to load older messages // Let's start to load older messages
_setLoadingOlderMessagesState(_OlderMessagesLevel.LOADING); _setLoadingOlderMessagesState(_OlderMessagesLevel.LOADING);
@ -147,12 +155,6 @@ class _ConversationScreenState extends SafeState<ConversationScreen> {
// Mark as not loading anymore // Mark as not loading anymore
_setLoadingOlderMessagesState(_OlderMessagesLevel.NONE); _setLoadingOlderMessagesState(_OlderMessagesLevel.NONE);
// Check for errors
if (messages == null) {
_errorLoading();
return;
}
// Check if there is no more unread messages // Check if there is no more unread messages
if (messages.length == 0) { if (messages.length == 0) {
_setLoadingOlderMessagesState(_OlderMessagesLevel.NO_MORE_AVAILABLE); _setLoadingOlderMessagesState(_OlderMessagesLevel.NO_MORE_AVAILABLE);
@ -161,20 +163,24 @@ class _ConversationScreenState extends SafeState<ConversationScreen> {
// Apply the messages // Apply the messages
_applyNewMessages(messages); _applyNewMessages(messages);
} catch (e, s) {
print("Failed to load older messages! $e => $s");
_errorLoading();
}
} }
/// Apply new messages [messages] must not be null /// Apply new messages [messages] must not be null
///
/// Throws in case of failure
Future<void> _applyNewMessages(ConversationMessagesList messages) async { Future<void> _applyNewMessages(ConversationMessagesList messages) async {
// We ignore new messages once the area is no longer visible // We ignore new messages once the area is no longer visible
if (!this.mounted) return; if (!this.mounted) return;
//Then get information about users //Then get information about users
final usersToGet = final usersToGet =
findMissingFromList(_usersInfo.usersID, messages.getUsersID()); findMissingFromList(_usersInfo.usersID, messages.getUsersID()).toSet();
final users = await _usersHelper.getUsersInfo(usersToGet); final users = await _usersHelper.getList(usersToGet);
if (users == null) _errorLoading();
// Save the new list of messages // Save the new list of messages
setState(() { setState(() {

View File

@ -62,32 +62,24 @@ class _ConversationScreenState extends SafeState<ConversationsListScreen> {
await _loadConversationsList(false); await _loadConversationsList(false);
} }
void _gotLoadingError() {
setError(_list == null ? LoadErrorLevel.MAJOR : LoadErrorLevel.MINOR);
}
/// Load the list of conversations /// Load the list of conversations
Future<void> _loadConversationsList(bool cached) async { Future<void> _loadConversationsList(bool cached) async {
setError(LoadErrorLevel.NONE); setError(LoadErrorLevel.NONE);
//Get the list of conversations try {
var list; ConversationsList list = cached
if (cached) ? await _conversationsHelper.getCachedList()
list = await _conversationsHelper.getCachedList(); : await _conversationsHelper.downloadList();
else assert(list != null);
list = await _conversationsHelper.downloadList();
if (list == null) return _gotLoadingError();
//Get information about the members of the conversations //Get information about the members of the conversations
list.users = await _usersHelper.getUsersInfo(list.allUsersID); list.users = await _usersHelper.getList(list.allUsersID);
if (list.users == null) return _gotLoadingError(); setState(() => _list = list);
} catch (e, s) {
//Save list debugPrint("Failed to get conversations list! $e => $s", wrapWidth: 1024);
setState(() { setError(_list == null ? LoadErrorLevel.MAJOR : LoadErrorLevel.MINOR);
_list = list; }
});
} }
/// Build an error card /// Build an error card
@ -159,9 +151,13 @@ class _ConversationScreenState extends SafeState<ConversationsListScreen> {
if (result == null || !result) return; if (result == null || !result) return;
// Request the conversation to be deleted now // 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( Scaffold.of(context).showSnackBar(
SnackBar(content: Text(tr("Could not delete the conversation!")))); SnackBar(content: Text(tr("Could not delete the conversation!"))));
}
// Reload the list of conversations // Reload the list of conversations
_loadConversationsList(false); _loadConversationsList(false);

View File

@ -3,11 +3,8 @@ import 'package:comunic/helpers/events_helper.dart';
import 'package:comunic/helpers/users_helper.dart'; import 'package:comunic/helpers/users_helper.dart';
import 'package:comunic/lists/unread_conversations_list.dart'; import 'package:comunic/lists/unread_conversations_list.dart';
import 'package:comunic/lists/users_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/async_screen_widget.dart';
import 'package:comunic/ui/widgets/safe_state.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:comunic/utils/intl_utils.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -70,7 +67,7 @@ class _UnreadConversationsScreenState
} }
Widget _tileBuilder(BuildContext context, int index) { Widget _tileBuilder(BuildContext context, int index) {
final conv = _list[index]; /*final conv = _list[index];
final user = _users.getUser(conv.userID); final user = _users.getUser(conv.userID);
return ListTile( return ListTile(
leading: AccountImageWidget(user: user), leading: AccountImageWidget(user: user),
@ -83,9 +80,12 @@ class _UnreadConversationsScreenState
style: TextStyle(fontStyle: FontStyle.italic), style: TextStyle(fontStyle: FontStyle.italic),
), ),
]), ]),
), )
trailing: Text(diffTimeFromNowToStr(conv.lastActive)), trailing: Text(diffTimeFromNowToStr(conv.lastActive)),
onTap: () => MainController.of(context).openConversation(conv.id), onTap: () => MainController.of(context).openConversation(conv.id),
); );*/
// TODO : reimplement
throw new Exception("unimplemented");
} }
} }

View File

@ -1,8 +1,6 @@
import 'package:comunic/helpers/conversations_helper.dart';
import 'package:comunic/lists/users_list.dart'; import 'package:comunic/lists/users_list.dart';
import 'package:comunic/models/conversation.dart'; import 'package:comunic/models/conversation.dart';
import 'package:comunic/models/user.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/tiles/simple_user_tile.dart';
import 'package:comunic/ui/widgets/pick_user_widget.dart'; import 'package:comunic/ui/widgets/pick_user_widget.dart';
import 'package:comunic/utils/intl_utils.dart'; import 'package:comunic/utils/intl_utils.dart';
@ -36,11 +34,11 @@ class _UpdateConversationScreen extends State<UpdateConversationScreen> {
get isUpdating => widget.initialSettings != null; get isUpdating => widget.initialSettings != null;
get isOwner => !isUpdating || widget.initialSettings.isOwner; get isAdmin => !isUpdating || widget.initialSettings.isAdmin;
Conversation get _initialSettings => widget.initialSettings; Conversation get _initialSettings => widget.initialSettings;
bool get _canAddMembers => isOwner || _initialSettings.canEveryoneAddMembers; bool get _canAddMembers => isAdmin || _initialSettings.canEveryoneAddMembers;
@override @override
void initState() { void initState() {
@ -68,7 +66,7 @@ class _UpdateConversationScreen extends State<UpdateConversationScreen> {
decoration: InputDecoration( decoration: InputDecoration(
labelText: tr("Conversation name (optionnal)"), labelText: tr("Conversation name (optionnal)"),
alignLabelWithHint: true, alignLabelWithHint: true,
enabled: isOwner, enabled: isAdmin,
), ),
), ),
@ -90,7 +88,7 @@ class _UpdateConversationScreen extends State<UpdateConversationScreen> {
children: <Widget>[ children: <Widget>[
Switch.adaptive( Switch.adaptive(
value: _canEveryoneAddMembers, value: _canEveryoneAddMembers,
onChanged: isOwner onChanged: isAdmin
? (b) => setState(() { ? (b) => setState(() {
_canEveryoneAddMembers = b; _canEveryoneAddMembers = b;
}) })
@ -126,7 +124,7 @@ class _UpdateConversationScreen extends State<UpdateConversationScreen> {
PopupMenuItem( PopupMenuItem(
child: Text(tr("Remove")), child: Text(tr("Remove")),
value: _MembersMenuChoices.REMOVE, value: _MembersMenuChoices.REMOVE,
enabled: isOwner || enabled: isAdmin ||
(_canEveryoneAddMembers && (_canEveryoneAddMembers &&
!_initialSettings.members !_initialSettings.members
.contains(f.id)), .contains(f.id)),
@ -163,7 +161,8 @@ class _UpdateConversationScreen extends State<UpdateConversationScreen> {
/// Submit the conversation /// Submit the conversation
Future<void> _submitForm() async { Future<void> _submitForm() async {
final settings = Conversation( // TODO : reimplement
/* final settings = Conversation(
id: isUpdating ? widget.initialSettings.id : 0, id: isUpdating ? widget.initialSettings.id : 0,
ownerID: isUpdating ? widget.initialSettings.ownerID : 0, ownerID: isUpdating ? widget.initialSettings.ownerID : 0,
name: _nameController.text, name: _nameController.text,
@ -198,6 +197,6 @@ class _UpdateConversationScreen extends State<UpdateConversationScreen> {
MainController.of(context).popPage(); MainController.of(context).popPage();
if (!isUpdating) if (!isUpdating)
MainController.of(context).openConversation(conversationID); MainController.of(context).openConversation(conversationID);*/
} }
} }

View File

@ -1,7 +1,6 @@
import 'package:comunic/models/conversation_message.dart'; import 'package:comunic/models/conversation_message.dart';
import 'package:comunic/models/user.dart'; import 'package:comunic/models/user.dart';
import 'package:comunic/ui/widgets/account_image_widget.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/ui/widgets/text_widget.dart';
import 'package:comunic/utils/date_utils.dart'; import 'package:comunic/utils/date_utils.dart';
import 'package:comunic/utils/intl_utils.dart'; import 'package:comunic/utils/intl_utils.dart';
@ -72,7 +71,9 @@ class ConversationMessageTile extends StatelessWidget {
/// Build widget image /// Build widget image
Widget _buildMessageImage(BuildContext context) { Widget _buildMessageImage(BuildContext context) {
return Container( return Text("");
// TODO : fix file
/*return Container(
margin: EdgeInsets.only(bottom: 2), margin: EdgeInsets.only(bottom: 2),
child: NetworkImageWidget( child: NetworkImageWidget(
url: message.imageURL, url: message.imageURL,
@ -80,7 +81,7 @@ class ConversationMessageTile extends StatelessWidget {
width: 200, width: 200,
height: 200, height: 200,
), ),
); );*/
} }
/// Build message date /// Build message date

View File

@ -80,7 +80,7 @@ class ConversationTile extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[ children: <Widget>[
_buildSubInformation( _buildSubInformation(
Icons.access_time, diffTimeFromNowToStr(conversation.lastActive)), Icons.access_time, diffTimeFromNowToStr(conversation.lastActivity)),
_buildSubInformation( _buildSubInformation(
Icons.group, Icons.group,
conversation.members.length == 1 conversation.members.length == 1

View File

@ -119,6 +119,7 @@ class PostsListWidgetState extends SafeState<PostsListWidget> {
_loading = true; _loading = true;
try {
final list = !getOlder final list = !getOlder
? await widget.getPostsList() ? await widget.getPostsList()
: await widget.getOlder(_list.oldestID); : await widget.getOlder(_list.oldestID);
@ -127,8 +128,6 @@ class PostsListWidgetState extends SafeState<PostsListWidget> {
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();
@ -146,6 +145,10 @@ class PostsListWidgetState extends SafeState<PostsListWidget> {
_groups.addAll(groups); _groups.addAll(groups);
} }
}); });
} catch (e, s) {
print("Failed to load post information ! $e => $s");
_loadError();
}
_loading = false; _loading = false;
} }

View File

@ -81,7 +81,7 @@ class _ConversationWindowState extends SafeState<ConversationWindow> {
_refresh(); _refresh();
listen<NewConversationMessageEvent>((e) { listen<NewConversationMessageEvent>((e) {
if (e.msg.conversationID == _convID && if (e.msg.convID == _convID &&
_collapsed && _collapsed &&
e.msg.userID != userID()) setState(() => _hasNewMessages = true); e.msg.userID != userID()) setState(() => _hasNewMessages = true);
}); });

View File

@ -10,16 +10,16 @@ import 'package:flutter/material.dart';
/// Open a private conversation with a given [userID] /// Open a private conversation with a given [userID]
Future<bool> openPrivateConversation(BuildContext context, int userID) async { Future<bool> openPrivateConversation(BuildContext context, int userID) async {
try {
final convID = await ConversationsHelper().getPrivate(userID); final convID = await ConversationsHelper().getPrivate(userID);
if (convID == null) {
showSimpleSnack(context, tr("Could not find a private conversation!"));
return false;
}
// Open the conversation // Open the conversation
MainController.of(context).openConversation(convID); MainController.of(context).openConversation(convID);
// Success
return true; return true;
} catch (e, s) {
print("Failed to find private conversation! $e => $s");
showSimpleSnack(context, tr("Could not find a private conversation!"));
return false;
}
} }

View File

@ -346,7 +346,7 @@ packages:
source: hosted source: hosted
version: "1.8.0-nullsafety.1" version: "1.8.0-nullsafety.1"
path_provider: path_provider:
dependency: transitive dependency: "direct main"
description: description:
name: path_provider name: path_provider
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"

View File

@ -11,7 +11,7 @@ description: Comunic client
# In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion.
# Read more about iOS versioning at # Read more about iOS versioning at
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
version: 1.1.1+5 version: 1.1.2+6
environment: environment:
sdk: ">=2.1.0 <3.0.0" sdk: ">=2.1.0 <3.0.0"
@ -91,6 +91,8 @@ dependencies:
# Version manager # Version manager
version: ^1.2.0 version: ^1.2.0
path_provider: ^1.6.27
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:
sdk: flutter sdk: flutter