OpenContacts/lib/clients/messaging_client.dart

397 lines
13 KiB
Dart
Raw Permalink Normal View History

import 'dart:async';
2023-10-03 12:20:02 -04:00
import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';
import 'package:hive_flutter/hive_flutter.dart';
import 'package:logging/logging.dart';
import 'package:package_info_plus/package_info_plus.dart';
2024-07-15 00:23:04 -04:00
import 'package:OpenContacts/apis/contact_api.dart';
import 'package:OpenContacts/apis/message_api.dart';
import 'package:OpenContacts/apis/session_api.dart';
import 'package:OpenContacts/apis/user_api.dart';
import 'package:OpenContacts/clients/api_client.dart';
import 'package:OpenContacts/clients/notification_client.dart';
import 'package:OpenContacts/clients/settings_client.dart';
import 'package:OpenContacts/crypto_helper.dart';
import 'package:OpenContacts/hub_manager.dart';
import 'package:OpenContacts/models/hub_events.dart';
import 'package:OpenContacts/models/message.dart';
import 'package:OpenContacts/models/session.dart';
import 'package:OpenContacts/models/users/friend.dart';
import 'package:OpenContacts/models/users/online_status.dart';
import 'package:OpenContacts/models/users/user_status.dart';
class MessagingClient extends ChangeNotifier {
static const Duration _autoRefreshDuration = Duration(seconds: 10);
static const Duration _unreadSafeguardDuration = Duration(seconds: 120);
static const Duration _statusHeartbeatDuration = Duration(seconds: 150);
static const String _messageBoxKey = "message-box";
static const String _lastUpdateKey = "__last-update-time";
2023-05-02 04:04:54 -04:00
final ApiClient _apiClient;
final List<Friend> _sortedFriendsCache = []; // Keep a sorted copy so as to not have to sort during build()
2023-05-02 04:04:54 -04:00
final Map<String, MessageCache> _messageCache = {};
final Map<String, List<Message>> _unreads = {};
2023-09-29 09:33:43 -04:00
final Logger _logger = Logger("Messaging");
2023-05-05 05:29:54 -04:00
final NotificationClient _notificationClient;
2023-09-29 09:33:43 -04:00
final HubManager _hubManager = HubManager();
final Map<String, Session> _sessionMap = {};
2023-10-10 03:53:34 -04:00
final Set<String> _knownSessionKeys = {};
final SettingsClient _settingsClient;
Friend? selectedFriend;
2023-09-30 09:22:37 -04:00
Timer? _statusHeartbeat;
Timer? _autoRefresh;
Timer? _unreadSafeguard;
String? _initStatus;
UserStatus _userStatus = UserStatus.initial();
2023-09-30 09:22:37 -04:00
UserStatus get userStatus => _userStatus;
MessagingClient(
{required ApiClient apiClient,
required NotificationClient notificationClient,
required SettingsClient settingsClient})
2023-09-29 09:33:43 -04:00
: _apiClient = apiClient,
_notificationClient = notificationClient,
_settingsClient = settingsClient {
2023-06-03 11:17:54 -04:00
debugPrint("mClient created: $hashCode");
Hive.openBox(_messageBoxKey).then((box) async {
await box.delete(_lastUpdateKey);
2023-10-10 03:53:34 -04:00
final sessions = await SessionApi.getSessions(_apiClient);
_sessionMap.addEntries(sessions.map((e) => MapEntry(e.id, e)));
_setupHub();
});
}
@override
void dispose() {
2023-06-03 11:17:54 -04:00
debugPrint("mClient disposed: $hashCode");
_autoRefresh?.cancel();
_statusHeartbeat?.cancel();
2023-05-25 14:19:03 -04:00
_unreadSafeguard?.cancel();
2023-09-29 09:33:43 -04:00
_hubManager.dispose();
super.dispose();
}
String? get initStatus => _initStatus;
2023-05-03 14:03:46 -04:00
List<Friend> get cachedFriends => _sortedFriendsCache;
List<Message> getUnreadsForFriend(Friend friend) => _unreads[friend.id] ?? [];
bool friendHasUnreads(Friend friend) => _unreads.containsKey(friend.id);
2023-09-29 09:33:43 -04:00
bool messageIsUnread(Message message) =>
_unreads[message.senderId]?.any((element) => element.id == message.id) ?? false;
Friend? getAsFriend(String userId) => Friend.fromMapOrNull(Hive.box(_messageBoxKey).get(userId));
MessageCache? getUserMessageCache(String userId) => _messageCache[userId];
MessageCache _createUserMessageCache(String userId) => MessageCache(apiClient: _apiClient, userId: userId);
2023-09-29 09:33:43 -04:00
Future<void> refreshFriendsListWithErrorHandler() async {
try {
await refreshFriendsList();
} catch (e) {
_initStatus = "$e";
notifyListeners();
}
}
Future<void> refreshFriendsList() async {
DateTime? lastUpdateUtc = Hive.box(_messageBoxKey).get(_lastUpdateKey);
_autoRefresh?.cancel();
_autoRefresh = Timer(_autoRefreshDuration, () => refreshFriendsList());
2023-09-29 07:17:17 -04:00
final friends = await ContactApi.getFriendsList(_apiClient, lastStatusUpdate: lastUpdateUtc);
for (final friend in friends) {
2023-09-29 09:33:43 -04:00
await _updateContact(friend);
}
2023-05-06 12:34:14 -04:00
_initStatus = "";
notifyListeners();
}
2023-09-29 07:17:17 -04:00
void sendMessage(Message message) {
final msgBody = message.toMap();
2023-09-29 09:33:43 -04:00
_hubManager.send("SendMessage", arguments: [msgBody]);
final cache = getUserMessageCache(message.recipientId) ?? _createUserMessageCache(message.recipientId);
cache.addMessage(message);
notifyListeners();
}
void markMessagesRead(MarkReadBatch batch) {
if (_userStatus.onlineStatus == OnlineStatus.invisible || _userStatus.onlineStatus == OnlineStatus.offline) return;
final msgBody = batch.toMap();
2023-09-29 09:33:43 -04:00
_hubManager.send("MarkMessagesRead", arguments: [msgBody]);
clearUnreadsForUser(batch.senderId);
}
2023-10-10 03:53:34 -04:00
Future<void> setOnlineStatus(OnlineStatus status) async {
2023-09-30 08:20:08 -04:00
final pkginfo = await PackageInfo.fromPlatform();
final now = DateTime.now();
2023-10-03 12:20:02 -04:00
_userStatus = _userStatus.copyWith(
userId: _apiClient.userId,
2023-09-30 08:20:08 -04:00
appVersion: "${pkginfo.version} of ${pkginfo.appName}",
lastPresenceTimestamp: now,
lastStatusChange: now,
2023-10-10 03:53:34 -04:00
onlineStatus: status,
isPresent: true,
2023-09-30 08:20:08 -04:00
);
_hubManager.send(
"BroadcastStatus",
arguments: [
_userStatus.toMap(),
{
2023-10-03 12:20:02 -04:00
"group": 1,
"targetIds": null,
}
],
);
2023-09-30 09:22:37 -04:00
final self = getAsFriend(_apiClient.userId);
2023-10-03 12:20:02 -04:00
if (self != null) {
await _updateContact(self.copyWith(userStatus: _userStatus));
}
2023-09-30 09:22:37 -04:00
notifyListeners();
2023-09-30 08:20:08 -04:00
}
void addUnread(Message message) {
var messages = _unreads[message.senderId];
if (messages == null) {
messages = [message];
_unreads[message.senderId] = messages;
} else {
messages.add(message);
}
messages.sort();
2023-05-06 12:34:14 -04:00
_sortFriendsCache();
_notificationClient.showUnreadMessagesNotification(messages.reversed);
notifyListeners();
}
void updateAllUnreads(List<Message> messages) {
_unreads.clear();
for (final msg in messages) {
if (msg.senderId != _apiClient.userId) {
final value = _unreads[msg.senderId];
if (value == null) {
_unreads[msg.senderId] = [msg];
} else {
value.add(msg);
}
}
}
}
void clearUnreadsForUser(String userId) {
_unreads[userId]?.clear();
notifyListeners();
}
2023-05-16 09:57:44 -04:00
void deleteUserMessageCache(String userId) {
_messageCache.remove(userId);
}
Future<void> loadUserMessageCache(String userId) async {
final cache = getUserMessageCache(userId) ?? _createUserMessageCache(userId);
await cache.loadMessages();
_messageCache[userId] = cache;
notifyListeners();
}
Future<void> updateFriendStatus(String userId) async {
final friend = getAsFriend(userId);
if (friend == null) return;
final newStatus = await UserApi.getUserStatus(_apiClient, userId: userId);
2023-09-29 09:33:43 -04:00
await _updateContact(friend.copyWith(userStatus: newStatus));
notifyListeners();
}
void resetInitStatus() {
_initStatus = null;
notifyListeners();
}
Future<void> _refreshUnreads() async {
try {
final unreadMessages = await MessageApi.getUserMessages(_apiClient, unreadOnly: true);
updateAllUnreads(unreadMessages.toList());
} catch (_) {}
}
// Calculate online status value, with 'headless' between 'busy' and 'offline'
double getOnlineStatusValue(Friend friend) {
// Adjusting values to ensure correct placement of 'headless'
if (friend.isHeadless) return 2.5;
switch (friend.userStatus.onlineStatus) {
2024-07-15 00:09:15 -04:00
case OnlineStatus.sociable:
return 0;
2024-07-15 00:09:15 -04:00
case OnlineStatus.online:
return 1;
2024-07-15 00:09:15 -04:00
case OnlineStatus.away:
return 2;
2024-07-15 00:09:15 -04:00
case OnlineStatus.busy:
return 3;
case OnlineStatus.invisible:
2024-07-15 00:09:15 -04:00
return 3.5;
case OnlineStatus.offline:
default:
2024-07-15 00:09:15 -04:00
return 4;
}
}
void _sortFriendsCache() {
_sortedFriendsCache.sort((a, b) {
// Check for unreads and sort by latest message time if either has unreads
bool aHasUnreads = friendHasUnreads(a);
bool bHasUnreads = friendHasUnreads(b);
if (aHasUnreads || bHasUnreads) {
if (aHasUnreads && bHasUnreads) {
return -a.latestMessageTime.compareTo(b.latestMessageTime);
}
return aHasUnreads ? -1 : 1;
}
int onlineStatusComparison = getOnlineStatusValue(a).compareTo(getOnlineStatusValue(b));
if (onlineStatusComparison != 0) {
return onlineStatusComparison;
}
return -a.latestMessageTime.compareTo(b.latestMessageTime);
});
}
2023-09-29 09:33:43 -04:00
Future<void> _updateContact(Friend friend) async {
final box = Hive.box(_messageBoxKey);
box.put(friend.id, friend.toMap());
final lastStatusUpdate = box.get(_lastUpdateKey);
if (lastStatusUpdate == null || friend.userStatus.lastStatusChange.isAfter(lastStatusUpdate)) {
await box.put(_lastUpdateKey, friend.userStatus.lastStatusChange);
}
final sIndex = _sortedFriendsCache.indexWhere((element) => element.id == friend.id);
if (sIndex == -1) {
_sortedFriendsCache.add(friend);
} else {
_sortedFriendsCache[sIndex] = friend;
}
if (friend.id == selectedFriend?.id) {
selectedFriend = friend;
}
_sortFriendsCache();
}
2023-09-29 09:33:43 -04:00
Future<void> _setupHub() async {
2023-05-02 04:04:54 -04:00
if (!_apiClient.isAuthenticated) {
2023-09-30 06:22:32 -04:00
_logger.info("Tried to connect to Resonite Hub without authentication, this is probably fine for now.");
return;
}
2023-09-29 09:33:43 -04:00
_hubManager.setHeaders(_apiClient.authorizationHeader);
_hubManager.setHandler(EventTarget.messageSent, _onMessageSent);
_hubManager.setHandler(EventTarget.receiveMessage, _onReceiveMessage);
_hubManager.setHandler(EventTarget.messagesRead, _onMessagesRead);
_hubManager.setHandler(EventTarget.receiveStatusUpdate, _onReceiveStatusUpdate);
2023-09-30 06:22:32 -04:00
_hubManager.setHandler(EventTarget.receiveSessionUpdate, _onReceiveSessionUpdate);
2023-10-03 12:36:41 -04:00
_hubManager.setHandler(EventTarget.removeSession, _onRemoveSession);
2023-09-29 09:33:43 -04:00
await _hubManager.start();
_hubManager.send(
"InitializeStatus",
responseHandler: (Map data) async {
final rawContacts = data["contacts"] as List;
final contacts = rawContacts.map((e) => Friend.fromMap(e)).toList();
for (final contact in contacts) {
await _updateContact(contact);
}
_initStatus = "";
notifyListeners();
await _refreshUnreads();
_unreadSafeguard = Timer.periodic(_unreadSafeguardDuration, (timer) => _refreshUnreads());
_hubManager.send("RequestStatus", arguments: [null, false]);
final lastOnline =
OnlineStatus.values.elementAtOrNull(_settingsClient.currentSettings.lastOnlineStatus.valueOrDefault);
await setOnlineStatus(lastOnline ?? OnlineStatus.online);
_statusHeartbeat = Timer.periodic(_statusHeartbeatDuration, (timer) {
setOnlineStatus(_userStatus.onlineStatus);
});
2023-09-29 09:33:43 -04:00
},
);
}
2023-10-03 12:20:02 -04:00
Map<String, Session> createSessionMap(String salt) {
return _sessionMap.map((key, value) => MapEntry(CryptoHelper.idHash(value.id + salt), value));
}
2023-09-29 09:33:43 -04:00
void _onMessageSent(List args) {
final msg = args[0];
final message = Message.fromMap(msg, withState: MessageState.sent);
final cache = getUserMessageCache(message.recipientId) ?? _createUserMessageCache(message.recipientId);
cache.addMessage(message);
notifyListeners();
}
2023-09-29 09:33:43 -04:00
void _onReceiveMessage(List args) {
final msg = args[0];
final message = Message.fromMap(msg);
final cache = getUserMessageCache(message.senderId) ?? _createUserMessageCache(message.senderId);
cache.addMessage(message);
if (message.senderId != selectedFriend?.id) {
addUnread(message);
updateFriendStatus(message.senderId);
} else {
markMessagesRead(MarkReadBatch(senderId: message.senderId, ids: [message.id], readTime: DateTime.now()));
}
2023-09-29 09:33:43 -04:00
notifyListeners();
}
2023-09-29 09:33:43 -04:00
void _onMessagesRead(List args) {
final messageIds = args[0]["ids"] as List;
final recipientId = args[0]["recipientId"];
if (recipientId == null) return;
final cache = getUserMessageCache(recipientId);
if (cache == null) return;
for (var id in messageIds) {
cache.setMessageState(id, MessageState.read);
}
2023-09-29 09:33:43 -04:00
notifyListeners();
}
2023-09-29 09:33:43 -04:00
void _onReceiveStatusUpdate(List args) {
2023-10-03 12:36:41 -04:00
final statusUpdate = args[0];
var status = UserStatus.fromMap(statusUpdate);
final sessionMap = createSessionMap(status.hashSalt);
status = status.copyWith(
decodedSessions: status.sessions
.map((e) => sessionMap[e.sessionHash] ?? Session.none().copyWith(accessLevel: e.accessLevel))
.toList());
2023-10-03 12:36:41 -04:00
final friend = getAsFriend(statusUpdate["userId"])?.copyWith(userStatus: status);
if (friend != null) {
_updateContact(friend);
2023-09-29 09:33:43 -04:00
}
2023-10-10 03:53:34 -04:00
for (var session in status.sessions) {
if (session.broadcastKey != null && _knownSessionKeys.add(session.broadcastKey ?? "")) {
_hubManager.send("ListenOnKey", arguments: [session.broadcastKey]);
}
}
2023-09-29 09:33:43 -04:00
notifyListeners();
2023-05-03 14:03:46 -04:00
}
2023-09-30 06:22:32 -04:00
void _onReceiveSessionUpdate(List args) {
2023-10-03 12:36:41 -04:00
final sessionUpdate = args[0];
final session = Session.fromMap(sessionUpdate);
_sessionMap[session.id] = session;
notifyListeners();
}
void _onRemoveSession(List args) {
final session = args[0];
_sessionMap.remove(session);
notifyListeners();
}
2023-09-29 09:33:43 -04:00
}