moxxy/lib/service/conversation.dart

179 lines
5.8 KiB
Dart

import 'package:get_it/get_it.dart';
import 'package:moxxmpp/moxxmpp.dart';
import 'package:moxxyv2/service/database/database.dart';
import 'package:moxxyv2/service/not_specified.dart';
import 'package:moxxyv2/service/preferences.dart';
import 'package:moxxyv2/shared/models/conversation.dart';
import 'package:moxxyv2/shared/models/message.dart';
import 'package:synchronized/synchronized.dart';
typedef CreateConversationCallback = Future<Conversation> Function();
typedef UpdateConversationCallback = Future<Conversation> Function(Conversation);
typedef PreRunConversationCallback = Future<void> Function(Conversation?);
class ConversationService {
/// The list of known conversations.
Map<String, Conversation>? _conversationCache;
/// The lock for accessing _conversationCache
final Lock _lock = Lock();
/// When called with a JID [jid], then first, if non-null, [preRun] is
/// executed.
/// Next, if a conversation with JID [jid] exists, [update] is called with
/// the conversation as its argument. If not, then [create] is executed.
/// Returns either the result of [create], [update] or null.
Future<Conversation?> createOrUpdateConversation(
String jid, {
CreateConversationCallback? create,
UpdateConversationCallback? update,
PreRunConversationCallback? preRun,
}) async {
return _lock.synchronized(() async {
final conversation = await _getConversationByJid(jid);
// Pre run
if (preRun != null) {
await preRun(conversation);
}
if (conversation != null) {
// Conversation exists
if (update != null) {
return update(conversation);
}
} else {
// Conversation does not exist
if (create != null) {
return create();
}
}
return null;
});
}
/// Wrapper around DatabaseService's loadConversations that adds the loaded
/// to the cache.
Future<void> _loadConversationsIfNeeded() async {
if (_conversationCache != null) return;
final conversations = await GetIt.I.get<DatabaseService>().loadConversations();
_conversationCache = Map<String, Conversation>.fromEntries(
conversations.map((c) => MapEntry(c.jid, c)),
);
}
/// Returns the conversation with jid [jid] or null if not found.
Future<Conversation?> _getConversationByJid(String jid) async {
await _loadConversationsIfNeeded();
return _conversationCache![jid];
}
/// Wrapper around [ConversationService._getConversationByJid] that aquires
/// the lock for the cache.
Future<Conversation?> getConversationByJid(String jid) async {
return _lock.synchronized(() async => _getConversationByJid(jid));
}
/// For modifying the cache without writing it to disk. Useful, for example, when
/// changing the chat state.
void setConversation(Conversation conversation) {
_conversationCache![conversation.jid] = conversation;
}
/// Wrapper around [DatabaseService]'s [updateConversation] that modifies the cache.
/// To prevent issues with the cache, only call from within
/// [ConversationService.createOrUpdateConversation].
Future<Conversation> updateConversation(String jid, {
int? lastChangeTimestamp,
Message? lastMessage,
bool? open,
int? unreadCounter,
String? avatarUrl,
ChatState? chatState,
bool? muted,
bool? encrypted,
Object? contactId = notSpecified,
Object? contactAvatarPath = notSpecified,
Object? contactDisplayName = notSpecified,
}) async {
final conversation = (await _getConversationByJid(jid))!;
var newConversation = await GetIt.I.get<DatabaseService>().updateConversation(
jid,
lastMessage: lastMessage,
lastChangeTimestamp: lastChangeTimestamp,
open: open,
unreadCounter: unreadCounter,
avatarUrl: avatarUrl,
chatState: conversation.chatState,
muted: muted,
encrypted: encrypted,
contactId: contactId,
contactAvatarPath: contactAvatarPath,
contactDisplayName: contactDisplayName,
);
// Copy over the old lastMessage if a new one was not set
if (conversation.lastMessage != null && lastMessage == null) {
newConversation = newConversation.copyWith(lastMessage: conversation.lastMessage);
}
_conversationCache![jid] = newConversation;
return newConversation;
}
/// Wrapper around [DatabaseService]'s [addConversationFromData] that updates the
/// cache.
/// To prevent issues with the cache, only call from within
/// [ConversationService.createOrUpdateConversation].
Future<Conversation> addConversationFromData(
String title,
Message? lastMessage,
String avatarUrl,
String jid,
int unreadCounter,
int lastChangeTimestamp,
bool open,
bool muted,
bool encrypted,
String? contactId,
String? contactAvatarPath,
String? contactDisplayName,
) async {
final newConversation = await GetIt.I.get<DatabaseService>().addConversationFromData(
title,
lastMessage,
avatarUrl,
jid,
unreadCounter,
lastChangeTimestamp,
open,
muted,
encrypted,
contactId,
contactAvatarPath,
contactDisplayName,
);
if (_conversationCache != null) {
_conversationCache![newConversation.jid] = newConversation;
}
return newConversation;
}
/// Returns true if the stanzas to the conversation with [jid] should be encrypted.
/// If not, returns false.
///
/// If the conversation does not exist, then the value of the preference for
/// enableOmemoByDefault is used.
Future<bool> shouldEncryptForConversation(JID jid) async {
final prefs = await GetIt.I.get<PreferencesService>().getPreferences();
final conversation = await getConversationByJid(jid.toString());
return conversation?.encrypted ?? prefs.enableOmemoByDefault;
}
}