mirror of
https://gitlab.com/comunic/comunicmobile
synced 2024-11-24 13:59:22 +00:00
519 lines
18 KiB
Dart
519 lines
18 KiB
Dart
import 'package:comunic/helpers/groups_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';
|
|
import 'package:comunic/lists/conversations_list.dart';
|
|
import 'package:comunic/lists/unread_conversations_list.dart';
|
|
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:comunic/utils/color_utils.dart';
|
|
import 'package:comunic/utils/dart_color.dart';
|
|
import 'package:dio/dio.dart';
|
|
|
|
/// Conversation helper
|
|
///
|
|
/// @author Pierre HUBERT
|
|
|
|
enum SendMessageResult { SUCCESS, MESSAGE_REJECTED, FAILED }
|
|
|
|
class ConversationsHelper {
|
|
static final _registeredConversations = Map<int, int>();
|
|
|
|
/// Create a new conversation
|
|
///
|
|
/// 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: {
|
|
"name": settings.name,
|
|
"follow": settings.follow ? "true" : "false",
|
|
"users": settings.members.join(","),
|
|
"color": colorToHex(settings.color)
|
|
})
|
|
.addBool("canEveryoneAddMembers", settings.canEveryoneAddMembers)
|
|
.execWithThrow();
|
|
|
|
return response.getObject()["conversationID"];
|
|
}
|
|
|
|
/// 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();
|
|
|
|
/// 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();
|
|
|
|
/// 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();
|
|
|
|
/// Update an existing conversation
|
|
///
|
|
/// Throws in case of failure
|
|
static Future<void> updateConversation(
|
|
NewConversationsSettings settings) async {
|
|
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!)
|
|
.addString("color", colorToHex(settings.color));
|
|
|
|
await request.execWithThrow();
|
|
|
|
// Delete old conversation entry from the database
|
|
await ConversationsSerializationHelper()
|
|
.removeElement((t) => t.id == settings.convID);
|
|
}
|
|
|
|
/// 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();
|
|
|
|
/// 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]
|
|
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.withLogin("conversations/getList").execWithThrow();
|
|
|
|
ConversationsList list = ConversationsList();
|
|
response.getArray()!.forEach((f) => list.add(apiToConversation(f)));
|
|
|
|
// Update the database
|
|
await ConversationsSerializationHelper().setList(list);
|
|
|
|
return list;
|
|
}
|
|
|
|
/// Get the local list of conversations
|
|
Future<ConversationsList> getCachedList() async {
|
|
final list = await ConversationsSerializationHelper().getList();
|
|
list.sort();
|
|
return list;
|
|
}
|
|
|
|
/// Get information about a single conversation specified by its [id]
|
|
Future<Conversation> _downloadSingle(int? id) async {
|
|
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;
|
|
}
|
|
|
|
/// 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. The method throws an [Exception] in
|
|
/// case of failure
|
|
///
|
|
/// Return value of this method is never null.
|
|
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
|
|
/// about the users of this conversation
|
|
static String getConversationName(
|
|
Conversation conversation, UsersList? users) {
|
|
if (conversation.hasName) return conversation.name!;
|
|
|
|
String name = "";
|
|
int count = 0;
|
|
for (int i = 0; i < 3 && i < conversation.members!.length; i++)
|
|
if (conversation.members![i].userID != userID()) {
|
|
name += (count > 0 ? ", " : "") +
|
|
users!.getUser(conversation.members![i].userID).fullName;
|
|
count++;
|
|
}
|
|
|
|
if (conversation.members!.length > 3) name += ", ...";
|
|
|
|
return name;
|
|
}
|
|
|
|
/// 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",
|
|
needLogin: true,
|
|
args: {
|
|
"otherUser": userID.toString(),
|
|
"allowCreate": allowCreate.toString()
|
|
},
|
|
).execWithThrow();
|
|
|
|
// Get and return conversation ID
|
|
return int.parse(response.getObject()["conversationsID"][0].toString());
|
|
}
|
|
|
|
/// Asynchronously get the name of the conversation
|
|
///
|
|
/// Unlike the synchronous method, this method does not need information
|
|
/// about the members of the conversation
|
|
///
|
|
/// 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().getList(conversation.membersID);
|
|
|
|
return ConversationsHelper.getConversationName(conversation, members);
|
|
}
|
|
|
|
/// Turn an API entry into a [Conversation] object
|
|
static Conversation apiToConversation(Map<String, dynamic> map) {
|
|
return Conversation(
|
|
id: map["id"],
|
|
lastActivity: map["last_activity"],
|
|
name: map["name"],
|
|
color: map["color"] == null ? null : HexColor(map["color"]),
|
|
logoURL: map["logo"],
|
|
groupID: map["group_id"],
|
|
groupMinMembershipLevel:
|
|
APIGroupsMembershipLevelsMap[map["group_min_membership_level"]],
|
|
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"]
|
|
? CallCapabilities.AUDIO
|
|
: CallCapabilities.NONE),
|
|
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 {
|
|
response.assertOk();
|
|
|
|
// Parse the response of the server
|
|
ConversationMessagesList list = ConversationMessagesList();
|
|
response.getArray()!.forEach((f) {
|
|
list.add(
|
|
apiToConversationMessage(f),
|
|
);
|
|
});
|
|
|
|
// Save messages in the cache
|
|
await ConversationsMessagesSerializationHelper(conversationID)
|
|
.insertOrReplaceAll(list);
|
|
|
|
return list;
|
|
}
|
|
|
|
/// 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
|
|
///
|
|
/// Throws an exception in case of failure
|
|
Future<ConversationMessagesList> _downloadNewMessagesSingle(
|
|
int conversationID,
|
|
{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()
|
|
}).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.withLogin("conversations/get_older_messages", args: {
|
|
"conversationID": conversationID.toString(),
|
|
"oldest_message_id": oldestMessagesID.toString(),
|
|
"limit": limit.toString()
|
|
}).execWithThrow();
|
|
|
|
return await _parseConversationMessageFromServer(conversationID, response);
|
|
}
|
|
|
|
/// 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
|
|
///
|
|
/// Throws in case of failure
|
|
Future<ConversationMessagesList> getNewMessages(
|
|
{required int conversationID,
|
|
int? lastMessageID = 0,
|
|
bool online = true}) async {
|
|
if (online)
|
|
return await _downloadNewMessagesSingle(conversationID,
|
|
lastMessageID: lastMessageID);
|
|
else
|
|
return await ConversationsMessagesSerializationHelper(conversationID)
|
|
.getList();
|
|
}
|
|
|
|
/// Send a new message to the server
|
|
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;
|
|
|
|
// Check for file
|
|
if (message.hasFile) request.addBytesFile("file", message.file);
|
|
|
|
if (message.hasThumbnail)
|
|
request.addBytesFile("thumbnail", message.thumbnail);
|
|
|
|
//Send the message
|
|
APIResponse response;
|
|
if (!message.hasFile)
|
|
response = await request.exec();
|
|
else
|
|
response = await request.execWithFiles();
|
|
|
|
if (response.code == 401)
|
|
return SendMessageResult.MESSAGE_REJECTED;
|
|
else if (response.code != 200) return SendMessageResult.FAILED;
|
|
|
|
return SendMessageResult.SUCCESS;
|
|
}
|
|
|
|
/// Save / Update a message into the database
|
|
Future<void> saveMessage(ConversationMessage msg) async =>
|
|
await ConversationsMessagesSerializationHelper(msg.convID)
|
|
.insertOrReplace(msg);
|
|
|
|
/// Remove a message from the database
|
|
Future<void> removeMessage(ConversationMessage msg) async =>
|
|
await ConversationsMessagesSerializationHelper(msg.convID).remove(msg);
|
|
|
|
/// 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;
|
|
|
|
return true;
|
|
}
|
|
|
|
/// Delete permanently a message specified by its [id]
|
|
Future<bool> deleteMessage(int? id) async {
|
|
// Delete the message online
|
|
final response = await APIRequest(
|
|
uri: "conversations/deleteMessage",
|
|
needLogin: true,
|
|
args: {"messageID": id.toString()}).exec();
|
|
|
|
if (response.code != 200) return false;
|
|
|
|
return true;
|
|
}
|
|
|
|
/// 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(
|
|
conv: apiToConversation(f["conv"]),
|
|
message: apiToConversationMessage(f["message"]),
|
|
)));
|
|
}
|
|
|
|
/// 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.update(id, (value) => value + 1);
|
|
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.update(id, (value) => value - 1);
|
|
|
|
if (_registeredConversations[id]! <= 0) {
|
|
_registeredConversations.remove(id);
|
|
await ws("\$main/unregister_conv", {"convID": id});
|
|
}
|
|
}
|
|
|
|
/// 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});
|
|
|
|
/// Turn an API response into a ConversationMessage object
|
|
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"];
|
|
|
|
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!");
|
|
}
|
|
|
|
serverMessage = ConversationServerMessage(
|
|
type: messageType,
|
|
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);
|
|
}
|
|
}
|