1
0
mirror of https://gitlab.com/comunic/comunicmobile synced 2024-11-29 08:16:28 +00:00
comunicmobile/lib/helpers/conversations_helper.dart

520 lines
17 KiB
Dart
Raw Normal View History

2021-04-06 15:04:55 +00:00
import 'package:comunic/helpers/groups_helper.dart';
2021-03-10 16:54:41 +00:00
import 'package:comunic/helpers/serialization/conversation_message_serialization_helper.dart';
import 'package:comunic/helpers/serialization/conversations_serialization_helper.dart';
2019-04-24 15:46:25 +00:00
import 'package:comunic/helpers/users_helper.dart';
2020-04-19 11:42:47 +00:00
import 'package:comunic/helpers/websocket_helper.dart';
2019-04-25 06:56:16 +00:00
import 'package:comunic/lists/conversation_messages_list.dart';
import 'package:comunic/lists/conversations_list.dart';
import 'package:comunic/lists/unread_conversations_list.dart';
import 'package:comunic/lists/users_list.dart';
2019-04-23 12:35:41 +00:00
import 'package:comunic/models/api_request.dart';
2019-04-25 18:14:19 +00:00
import 'package:comunic/models/api_response.dart';
2019-04-23 12:35:41 +00:00
import 'package:comunic/models/conversation.dart';
2021-03-10 16:54:41 +00:00
import 'package:comunic/models/conversation_member.dart';
2019-04-25 06:56:16 +00:00
import 'package:comunic/models/conversation_message.dart';
2020-04-16 13:10:47 +00:00
import 'package:comunic/models/displayed_content.dart';
2021-03-10 16:54:41 +00:00
import 'package:comunic/models/new_conversation.dart';
2019-04-25 07:48:52 +00:00
import 'package:comunic/models/new_conversation_message.dart';
2021-03-10 16:54:41 +00:00
import 'package:comunic/models/new_conversation_settings.dart';
import 'package:comunic/models/unread_conversation.dart';
import 'package:comunic/utils/account_utils.dart';
2021-03-13 09:32:11 +00:00
import 'package:comunic/utils/color_utils.dart';
2021-03-10 17:04:29 +00:00
import 'package:comunic/utils/dart_color.dart';
2021-03-13 08:00:00 +00:00
import 'package:dio/dio.dart';
2019-04-27 06:51:58 +00:00
import 'package:meta/meta.dart';
2019-04-23 12:35:41 +00:00
/// Conversation helper
///
/// @author Pierre HUBERT
2019-04-25 07:48:52 +00:00
enum SendMessageResult { SUCCESS, MESSAGE_REJECTED, FAILED }
2019-04-23 12:35:41 +00:00
class ConversationsHelper {
2020-04-19 11:42:47 +00:00
static final _registeredConversations = Map<int, int>();
2019-04-27 14:23:08 +00:00
/// Create a new conversation
///
2021-03-13 09:32:11 +00:00
/// Return the ID of the newly created conversation
///
/// Throws in case of failure
static Future<int> createConversation(NewConversation settings) async {
final response = await APIRequest.withLogin("conversations/create", args: {
2021-03-10 16:54:41 +00:00
"name": settings.name ?? "",
"follow": settings.follow ? "true" : "false",
"users": settings.members.join(","),
2021-03-13 10:08:08 +00:00
"color": colorToHex(settings.color)
2021-03-13 09:32:11 +00:00
})
.addBool("canEveryoneAddMembers", settings.canEveryoneAddMembers)
.execWithThrow();
2019-04-27 14:23:08 +00:00
return response.getObject()["conversationID"];
}
2021-03-13 09:52:53 +00:00
/// Add a member to a conversation.
///
/// Throws in case of failure
static Future<void> addMember(int convID, int userID) async =>
await APIRequest.withLogin("conversations/addMember")
.addInt("convID", convID)
.addInt("userID", userID)
.execWithThrow();
2021-03-13 09:48:59 +00:00
/// Remove a member from a conversation.
///
/// Throws in case of failure
static Future<void> removeMember(int convID, int userID) async =>
await APIRequest.withLogin("conversations/removeMember")
.addInt("convID", convID)
.addInt("userID", userID)
.execWithThrow();
2021-03-13 10:02:44 +00:00
/// Update admin status of a user in a conversation
///
/// Throws in case of failure
static Future<void> setAdmin(int convID, int userID, bool admin) async =>
await APIRequest.withLogin("conversations/setAdmin")
.addInt("convID", convID)
.addInt("userID", userID)
.addBool("setAdmin", admin)
.execWithThrow();
2019-05-01 07:24:50 +00:00
/// Update an existing conversation
///
2021-03-10 16:54:41 +00:00
/// Throws in case of failure
2021-03-13 10:08:08 +00:00
static Future<void> updateConversation(
NewConversationsSettings settings) async {
2021-03-10 16:54:41 +00:00
final request = APIRequest.withLogin("conversations/updateSettings")
.addInt("conversationID", settings.convID)
.addBool("following", settings.following);
// Update conversation settings
if (settings.isComplete)
request
.addString("name", settings.name ?? "")
.addBool("canEveryoneAddMembers", settings.canEveryoneAddMembers)
2021-03-13 10:08:08 +00:00
.addString("color", colorToHex(settings.color));
2021-03-10 16:54:41 +00:00
await request.execWithThrow();
// Delete old conversation entry from the database
await ConversationsSerializationHelper()
.removeElement((t) => t.id == settings.convID);
2019-05-01 07:24:50 +00:00
}
2021-03-13 10:33:25 +00:00
/// Set a new conversation logo
///
/// Throws in case of failure
static Future<void> changeImage(int convID, BytesFile file) async =>
await APIRequest.withLogin("conversations/change_image")
.addInt("convID", convID)
.addBytesFile("file", file)
.execWithFilesAndThrow();
2021-03-13 10:42:58 +00:00
/// Remove conversation logo
///
/// Throws in case of failure
static Future<void> removeLogo(int convID) async =>
await APIRequest.withLogin("conversations/delete_image")
.addInt("convID", convID)
.execWithThrow();
/// Delete a conversation specified by its [id]
2021-03-10 16:54:41 +00:00
Future<void> deleteConversation(int id) async =>
await APIRequest.withLogin("conversations/delete")
.addInt("conversationID", id)
.execWithThrow();
2019-04-23 12:35:41 +00:00
/// Download the list of conversations from the server
2021-03-10 16:54:41 +00:00
///
/// Throws an exception in case of failure
Future<ConversationsList> downloadList() async {
2019-04-23 12:35:41 +00:00
final response =
2021-03-10 16:54:41 +00:00
await APIRequest.withLogin("conversations/getList").execWithThrow();
2019-04-23 12:35:41 +00:00
2021-03-10 16:54:41 +00:00
ConversationsList list = ConversationsList();
response.getArray().forEach((f) => list.add(apiToConversation(f)));
2019-04-23 12:35:41 +00:00
2021-03-10 16:54:41 +00:00
// Update the database
await ConversationsSerializationHelper().setList(list);
2019-04-23 12:35:41 +00:00
2021-03-10 16:54:41 +00:00
return list;
2019-04-23 12:35:41 +00:00
}
/// Get the local list of conversations
Future<ConversationsList> getCachedList() async {
2021-03-10 16:54:41 +00:00
final list = await ConversationsSerializationHelper().getList();
list.sort();
return list;
}
2019-04-24 15:46:25 +00:00
/// Get information about a single conversation specified by its [id]
Future<Conversation> _downloadSingle(int id) async {
2021-03-13 12:32:38 +00:00
final response = await APIRequest(
uri: "conversations/get_single",
needLogin: true,
args: {"conversationID": id.toString()}).execWithThrow();
final conversation = apiToConversation(response.getObject());
await ConversationsSerializationHelper()
.insertOrReplaceElement((c) => c.id == conversation.id, conversation);
return conversation;
2019-04-24 15:46:25 +00:00
}
2021-03-10 16:54:41 +00:00
/// Get information about a conversation. If [force] is set to false, a
2019-04-24 15:46:25 +00:00
/// cached version of the conversation will be used, else it will always get
2021-03-10 16:54:41 +00:00
/// the information from the server. The method throws an [Exception] in
2020-04-20 11:19:49 +00:00
/// case of failure
///
/// Return value of this method is never null.
2021-03-10 16:54:41 +00:00
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);
2020-04-20 11:19:49 +00:00
}
2019-04-24 15:46:25 +00:00
/// Get the name of a [conversation]. This requires information
/// about the users of this conversation
static String getConversationName(
Conversation conversation, UsersList users) {
2019-04-25 09:14:05 +00:00
if (conversation.hasName) return conversation.name;
String name = "";
int count = 0;
for (int i = 0; i < 3 && i < conversation.members.length; i++)
2021-03-10 16:54:41 +00:00
if (conversation.members[i].userID != userID()) {
name += (count > 0 ? ", " : "") +
2021-03-10 16:54:41 +00:00
users.getUser(conversation.members[i].userID).fullName;
count++;
}
if (conversation.members.length > 3) name += ", ...";
return name;
}
2019-04-24 15:46:25 +00:00
/// 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
2021-03-10 16:54:41 +00:00
///
/// Throws an exception in case of failure
Future<int> getPrivate(int userID, {bool allowCreate = true}) async {
final response = await APIRequest(
uri: "conversations/getPrivate",
needLogin: true,
args: {
"otherUser": userID.toString(),
"allowCreate": allowCreate.toString()
},
2021-03-10 16:54:41 +00:00
).execWithThrow();
// Get and return conversation ID
2021-03-10 16:54:41 +00:00
return int.parse(response.getObject()["conversationsID"][0].toString());
}
2020-04-20 11:19:49 +00:00
/// Asynchronously get the name of the conversation
2019-04-24 15:46:25 +00:00
///
/// Unlike the synchronous method, this method does not need information
/// about the members of the conversation
///
2021-03-10 16:54:41 +00:00
/// Throws an exception in case of failure
2019-04-24 15:46:25 +00:00
static Future<String> getConversationNameAsync(
Conversation conversation) async {
2019-04-25 09:14:05 +00:00
if (conversation.hasName) return conversation.name;
2019-04-24 15:46:25 +00:00
//Get information about the members of the conversation
2021-03-10 16:54:41 +00:00
final members = await UsersHelper().getList(conversation.membersID);
2019-04-24 15:46:25 +00:00
return ConversationsHelper.getConversationName(conversation, members);
}
/// Turn an API entry into a [Conversation] object
2020-05-05 16:49:50 +00:00
static Conversation apiToConversation(Map<String, dynamic> map) {
2019-04-24 15:46:25 +00:00
return Conversation(
2021-03-10 16:54:41 +00:00
id: map["id"],
lastActivity: map["last_activity"],
name: map["name"],
2021-03-10 17:04:29 +00:00
color: map["color"] == null ? null : HexColor(map["color"]),
2021-03-10 16:54:41 +00:00
logoURL: map["logo"],
groupID: map["group_id"],
2021-04-06 15:04:55 +00:00
groupMinMembershipLevel:
APIGroupsMembershipLevelsMap[map["group_min_membership_level"]],
2021-03-10 16:54:41 +00:00
members: map["members"]
.cast<Map<String, dynamic>>()
.map(apiToConversationMember)
.toList()
.cast<ConversationMember>(),
canEveryoneAddMembers: map["can_everyone_add_members"],
2020-04-20 08:37:59 +00:00
callCapabilities: map["can_have_video_call"]
? CallCapabilities.VIDEO
: (map["can_have_call"]
? CallCapabilities.AUDIO
: CallCapabilities.NONE),
isHavingCall: map["has_call_now"]);
2019-04-24 15:46:25 +00:00
}
2019-04-25 06:56:16 +00:00
2021-03-10 16:54:41 +00:00
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"],
);
2019-04-27 16:29:30 +00:00
/// Parse a list of messages given by the server
2021-03-10 16:54:41 +00:00
///
/// Throws an exception in case of failure
2019-04-27 16:29:30 +00:00
Future<ConversationMessagesList> _parseConversationMessageFromServer(
int conversationID, APIResponse response) async {
2021-03-10 16:54:41 +00:00
response.assertOk();
2019-04-27 16:29:30 +00:00
// Parse the response of the server
ConversationMessagesList list = ConversationMessagesList();
response.getArray().forEach((f) {
list.add(
2020-04-19 11:58:24 +00:00
apiToConversationMessage(f),
2019-04-27 16:29:30 +00:00
);
});
// Save messages in the cache
2021-03-10 16:54:41 +00:00
await ConversationsMessagesSerializationHelper(conversationID)
.insertOrReplaceAll(list);
2019-04-27 16:29:30 +00:00
return list;
}
2019-04-25 06:56:16 +00:00
/// Refresh the list of messages of a conversation
///
/// Set [lastMessageID] to 0 to specify that we do not have any message of the
/// conversation yet or another value else
2021-03-10 16:54:41 +00:00
///
/// Throws an exception in case of failure
2019-04-27 06:51:58 +00:00
Future<ConversationMessagesList> _downloadNewMessagesSingle(
int conversationID,
2019-04-25 06:56:16 +00:00
{int lastMessageID = 0}) async {
// Execute the request on the server
final response = await APIRequest(
uri: "conversations/refresh_single",
needLogin: true,
args: {
"conversationID": conversationID.toString(),
"last_message_id": lastMessageID.toString()
2021-03-10 16:54:41 +00:00
}).execWithThrow();
2019-04-25 06:56:16 +00:00
2019-04-27 16:29:30 +00:00
return await _parseConversationMessageFromServer(conversationID, response);
}
2019-04-25 06:56:16 +00:00
2019-04-27 16:29:30 +00:00
/// Get older messages for a given conversation from an online source
2021-03-10 16:54:41 +00:00
///
/// Throws in case of failure
2019-04-27 16:29:30 +00:00
Future<ConversationMessagesList> getOlderMessages({
@required int conversationID,
@required int oldestMessagesID,
int limit = 15,
}) async {
// Perform the request online
2021-03-10 16:54:41 +00:00
final response =
await APIRequest.withLogin("conversations/get_older_messages", args: {
"conversationID": conversationID.toString(),
"oldest_message_id": oldestMessagesID.toString(),
"limit": limit.toString()
}).execWithThrow();
2019-04-27 06:42:27 +00:00
2019-04-27 16:29:30 +00:00
return await _parseConversationMessageFromServer(conversationID, response);
2019-04-25 06:56:16 +00:00
}
2019-04-25 07:48:52 +00:00
2019-04-27 06:51:58 +00:00
/// Get new messages for a given conversation
///
/// 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
2021-03-10 16:54:41 +00:00
///
/// Throws in case of failure
2019-04-27 06:51:58 +00:00
Future<ConversationMessagesList> getNewMessages(
{@required int conversationID,
int lastMessageID = 0,
bool online = true}) async {
if (online)
return await _downloadNewMessagesSingle(conversationID,
lastMessageID: lastMessageID);
else
2021-03-10 16:54:41 +00:00
return await ConversationsMessagesSerializationHelper(conversationID)
.getList();
2019-05-04 08:24:38 +00:00
}
2019-04-25 07:48:52 +00:00
/// Send a new message to the server
2021-03-13 08:00:00 +00:00
Future<SendMessageResult> sendMessage(
NewConversationMessage message, {
ProgressCallback sendProgress,
CancelToken cancelToken,
}) async {
final request = APIRequest.withLogin("conversations/sendMessage")
.addInt("conversationID", message.conversationID)
.addString("message", message.hasMessage ? message.message : "");
request.progressCallback = sendProgress;
request.cancelToken = cancelToken;
2019-04-25 18:14:19 +00:00
2021-03-12 16:47:09 +00:00
// Check for file
if (message.hasFile) request.addBytesFile("file", message.file);
2019-04-25 18:14:19 +00:00
2021-03-12 18:36:42 +00:00
if (message.hasThumbnail)
request.addBytesFile("thumbnail", message.thumbnail);
2019-04-25 18:14:19 +00:00
//Send the message
APIResponse response;
2021-03-12 16:47:09 +00:00
if (!message.hasFile)
2019-04-25 18:14:19 +00:00
response = await request.exec();
else
response = await request.execWithFiles();
2019-04-25 07:48:52 +00:00
2019-04-27 06:42:27 +00:00
if (response.code == 401)
2019-04-25 07:48:52 +00:00
return SendMessageResult.MESSAGE_REJECTED;
2019-04-27 06:42:27 +00:00
else if (response.code != 200) return SendMessageResult.FAILED;
2019-04-25 07:48:52 +00:00
return SendMessageResult.SUCCESS;
}
2019-04-27 16:29:30 +00:00
2020-04-19 11:58:24 +00:00
/// Save / Update a message into the database
2021-03-10 16:54:41 +00:00
Future<void> saveMessage(ConversationMessage msg) async =>
await ConversationsMessagesSerializationHelper(msg.convID)
.insertOrReplace(msg);
2020-04-19 11:58:24 +00:00
2020-04-19 12:29:01 +00:00
/// Remove a message from the database
2021-03-10 16:54:41 +00:00
Future<void> removeMessage(ConversationMessage msg) async =>
await ConversationsMessagesSerializationHelper(msg.convID).remove(msg);
2020-04-19 12:29:01 +00:00
2019-05-04 08:24:38 +00:00
/// Update a message content
Future<bool> updateMessage(int id, String newContent) async {
final response = await APIRequest(
uri: "conversations/updateMessage",
needLogin: true,
args: {"messageID": id.toString(), "content": newContent}).exec();
if (response.code != 200) return false;
2020-04-19 12:16:35 +00:00
return true;
2019-05-04 08:24:38 +00:00
}
2019-05-04 06:58:14 +00:00
/// Delete permanently a message specified by its [id]
Future<bool> deleteMessage(int id) async {
// Delete the message online
final response = await APIRequest(
2019-05-04 08:24:38 +00:00
uri: "conversations/deleteMessage",
needLogin: true,
args: {"messageID": id.toString()}).exec();
2019-05-04 06:58:14 +00:00
2019-05-04 08:24:38 +00:00
if (response.code != 200) return false;
2019-05-04 06:58:14 +00:00
2020-04-19 12:29:01 +00:00
return true;
2019-05-04 06:58:14 +00:00
}
/// Get the list of unread conversations
///
/// Throws in case of failure
static Future<UnreadConversationsList> getListUnread() async {
final list = (await APIRequest.withLogin("conversations/get_list_unread")
.execWithThrow())
.getArray();
return UnreadConversationsList()
..addAll(list.map((f) => UnreadConversation(
2021-03-10 16:54:41 +00:00
conv: apiToConversation(f["conv"]),
message: apiToConversationMessage(f["message"]),
)));
}
2020-04-19 11:42:47 +00:00
/// Register a conversation : ask the server to notify about updates to the
/// conversation through WebSocket
Future<void> registerConversationEvents(int id) async {
if (_registeredConversations.containsKey(id))
_registeredConversations[id]++;
else {
_registeredConversations[id] = 1;
await ws("\$main/register_conv", {"convID": id});
}
}
/// Un-register to conversation update events
Future<void> unregisterConversationEvents(int id) async {
if (!_registeredConversations.containsKey(id)) return;
_registeredConversations[id]--;
if (_registeredConversations[id] <= 0) {
_registeredConversations.remove(id);
await ws("\$main/unregister_conv", {"convID": id});
}
}
2021-03-13 08:09:26 +00:00
/// Send a notification to inform that the user is writing a message
static Future<void> sendWritingEvent(int convID) async =>
await ws("conversations/is_writing", {"convID": convID});
2019-04-27 16:29:30 +00:00
/// Turn an API response into a ConversationMessage object
2020-04-19 11:58:24 +00:00
static ConversationMessage apiToConversationMessage(
Map<String, dynamic> map,
) {
2021-03-10 16:54:41 +00:00
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"];
var messageType;
switch (srvMessageMap["type"]) {
case "user_created_conv":
messageType = ConversationServerMessageType.USER_CREATED_CONVERSATION;
break;
case "user_added_another":
messageType = ConversationServerMessageType.USER_ADDED_ANOTHER_USER;
break;
case "user_left":
messageType = ConversationServerMessageType.USER_LEFT_CONV;
break;
case "user_removed_another":
messageType = ConversationServerMessageType.USER_REMOVED_ANOTHER_USER;
break;
default:
throw Exception(
"${srvMessageMap["type"]} is an unknown server message type!");
}
2021-03-10 16:54:41 +00:00
serverMessage = ConversationServerMessage(
type: messageType,
2021-03-10 16:54:41 +00:00
userID: srvMessageMap["user_id"],
userWhoAdded: srvMessageMap["user_who_added"],
userAdded: srvMessageMap["user_added"],
userWhoRemoved: srvMessageMap["user_who_removed"],
userRemoved: srvMessageMap["user_removed"],
);
}
2019-04-27 16:29:30 +00:00
return ConversationMessage(
2021-03-10 16:54:41 +00:00
id: map["id"],
convID: map["conv_id"],
userID: map["user_id"],
timeSent: map["time_sent"],
message: DisplayedString(map["message"] ?? ""),
file: file,
serverMessage: serverMessage);
2019-04-27 16:29:30 +00:00
}
2019-04-23 12:35:41 +00:00
}