1
0
mirror of https://gitlab.com/comunic/comunicmobile synced 2024-12-25 12:28:50 +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/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<int, int>();
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<int> createConversation(Conversation settings) async {
Future<int> 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<bool> 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<void> 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<bool> deleteConversation(int id) async {
final response = await APIRequest(
uri: "conversations/delete",
needLogin: true,
args: {
"conversationID": id.toString(),
},
).exec();
return response.code == 200;
}
Future<void> 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<ConversationsList> 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<ConversationsList> 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<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
/// the information from the server. The method throws an [Exception] in
/// case of failure
///
/// Return value of this method is never null.
Future<Conversation> 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<Conversation> 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<int> 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<String> 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<String, dynamic> 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<int>.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<String, dynamic>>()
.map(apiToConversationMember)
.toList()
.cast<ConversationMember>(),
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<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
///
/// Throws an exception in case of failure
Future<ConversationMessagesList> _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<ConversationMessagesList> _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<ConversationMessagesList> 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<ConversationMessagesList> 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<ConversationMessage> 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<void> saveMessage(ConversationMessage msg) async {
await _conversationMessagesDatabaseHelper.insertOrUpdate(msg);
}
Future<void> saveMessage(ConversationMessage msg) async =>
await ConversationsMessagesSerializationHelper(msg.convID)
.insertOrReplace(msg);
/// Remove a message from the database
Future<void> removeMessage(int msgID) async {
await _conversationMessagesDatabaseHelper.delete(msgID);
}
Future<void> removeMessage(ConversationMessage msg) async =>
await ConversationsMessagesSerializationHelper(msg.convID).remove(msg);
/// Update a message content
Future<bool> 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<String, dynamic> 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);
}
}

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";
}
/// 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";

View File

@ -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, "

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]
///
/// Throws in case of failure
Future<UsersList> getList(Set<int> 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

View File

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

View File

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

View File

@ -7,5 +7,9 @@ import 'package:comunic/models/unread_conversation.dart';
class UnreadConversationsList extends AbstractList<UnreadConversation> {
/// 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/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<Conversation> {
final int id;
final int lastActivity;
final String name;
final bool following;
final bool sawLastMessage;
final List<int> members;
final String color;
final String logoURL;
final int groupID;
final List<ConversationMember> 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<String, dynamic> 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<int> 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<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
callCapabilities = CallCapabilities.NONE,
isHavingCall = false,
super.fromMap(map);
isHavingCall = false;
@override
Map<String, dynamic> toMap() {
Map<String, dynamic> 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);
}
}

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/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<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 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<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 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<int> 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<String, dynamic> toMap() {
Map<String, dynamic> 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<String, dynamic> 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<String, dynamic> map)
: id = map["id"],
convID = map["convID"],
userID = map["userID"],
timeSent = map["timeSent"],
message = DisplayedString(map["message"]),
file = map["file"],
serverMessage = map["serverMessage"];
}

View File

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

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';
/// 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);
}

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/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<StatefulWidget> createState() => _ConversationRouteState();
}
class _ConversationRouteState extends State<ConversationRoute> {
class _ConversationRouteState extends SafeState<ConversationRoute> {
final ConversationsHelper _conversationsHelper = ConversationsHelper();
Conversation _conversation;
String _conversationName;
@ -42,21 +43,22 @@ class _ConversationRouteState extends State<ConversationRoute> {
Future<void> _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() {

View File

@ -42,19 +42,18 @@ class _UpdateConversationRoute extends State<UpdateConversationRoute> {
Future<void> _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

View File

@ -110,7 +110,7 @@ class _CallScreenState extends SafeState<CallScreen> {
// 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);

View File

@ -32,8 +32,7 @@ class _ConversationMembersScreenState extends State<ConversationMembersScreen> {
Future<void> _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<ConversationMembersScreen> {
);
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")),
);
}
}

View File

@ -70,9 +70,8 @@ class _ConversationScreenState extends SafeState<ConversationScreen> {
});
/// 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<void> _init() async {
@ -86,22 +85,27 @@ class _ConversationScreenState extends SafeState<ConversationScreen> {
.registerConversationEvents(widget.conversationID);
this.listen<NewConversationMessageEvent>((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<UpdatedConversationMessageEvent>((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<DeletedConversationMessageEvent>((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<ConversationScreen> {
Future<void> _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<ConversationScreen> {
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<void> _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(() {

View File

@ -62,32 +62,24 @@ class _ConversationScreenState extends SafeState<ConversationsListScreen> {
await _loadConversationsList(false);
}
void _gotLoadingError() {
setError(_list == null ? LoadErrorLevel.MAJOR : LoadErrorLevel.MINOR);
}
/// Load the list of conversations
Future<void> _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<ConversationsListScreen> {
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);

View File

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

View File

@ -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<UpdateConversationScreen> {
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<UpdateConversationScreen> {
decoration: InputDecoration(
labelText: tr("Conversation name (optionnal)"),
alignLabelWithHint: true,
enabled: isOwner,
enabled: isAdmin,
),
),
@ -90,7 +88,7 @@ class _UpdateConversationScreen extends State<UpdateConversationScreen> {
children: <Widget>[
Switch.adaptive(
value: _canEveryoneAddMembers,
onChanged: isOwner
onChanged: isAdmin
? (b) => setState(() {
_canEveryoneAddMembers = b;
})
@ -126,7 +124,7 @@ class _UpdateConversationScreen extends State<UpdateConversationScreen> {
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<UpdateConversationScreen> {
/// Submit the conversation
Future<void> _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<UpdateConversationScreen> {
MainController.of(context).popPage();
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/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

View File

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

View File

@ -119,33 +119,36 @@ class PostsListWidgetState extends SafeState<PostsListWidget> {
_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;
}

View File

@ -81,7 +81,7 @@ class _ConversationWindowState extends SafeState<ConversationWindow> {
_refresh();
listen<NewConversationMessageEvent>((e) {
if (e.msg.conversationID == _convID &&
if (e.msg.convID == _convID &&
_collapsed &&
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]
Future<bool> 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;
}

View File

@ -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"

View File

@ -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