2023-05-06 10:45:26 -04:00
|
|
|
import 'dart:async';
|
2023-09-29 09:33:43 -04:00
|
|
|
import 'package:contacts_plus_plus/hub_manager.dart';
|
2023-09-30 09:22:37 -04:00
|
|
|
import 'package:contacts_plus_plus/models/users/online_status.dart';
|
2023-09-29 09:33:43 -04:00
|
|
|
import 'package:contacts_plus_plus/models/users/user_status.dart';
|
2023-09-29 03:51:46 -04:00
|
|
|
import 'package:flutter/foundation.dart';
|
2023-05-17 07:54:30 -04:00
|
|
|
import 'package:flutter/widgets.dart';
|
|
|
|
import 'package:hive_flutter/hive_flutter.dart';
|
|
|
|
import 'package:logging/logging.dart';
|
|
|
|
|
2023-09-29 07:17:17 -04:00
|
|
|
import 'package:contacts_plus_plus/apis/contact_api.dart';
|
2023-05-03 14:03:46 -04:00
|
|
|
import 'package:contacts_plus_plus/apis/message_api.dart';
|
2023-05-06 12:34:14 -04:00
|
|
|
import 'package:contacts_plus_plus/apis/user_api.dart';
|
2023-05-05 06:45:00 -04:00
|
|
|
import 'package:contacts_plus_plus/clients/notification_client.dart';
|
2023-05-29 14:16:23 -04:00
|
|
|
import 'package:contacts_plus_plus/models/users/friend.dart';
|
2023-05-03 15:55:34 -04:00
|
|
|
import 'package:contacts_plus_plus/clients/api_client.dart';
|
2023-05-01 13:13:40 -04:00
|
|
|
import 'package:contacts_plus_plus/models/message.dart';
|
2023-09-30 08:20:08 -04:00
|
|
|
import 'package:package_info_plus/package_info_plus.dart';
|
2023-05-01 11:34:34 -04:00
|
|
|
|
|
|
|
enum EventType {
|
2023-09-29 07:17:17 -04:00
|
|
|
undefined,
|
|
|
|
invocation,
|
|
|
|
streamItem,
|
|
|
|
completion,
|
|
|
|
streamInvocation,
|
|
|
|
cancelInvocation,
|
|
|
|
ping,
|
|
|
|
close;
|
2023-05-01 11:34:34 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
enum EventTarget {
|
|
|
|
unknown,
|
|
|
|
messageSent,
|
2023-05-05 05:29:54 -04:00
|
|
|
receiveMessage,
|
2023-09-29 06:30:43 -04:00
|
|
|
messagesRead,
|
2023-09-29 07:17:17 -04:00
|
|
|
receiveSessionUpdate,
|
|
|
|
removeSession,
|
|
|
|
receiveStatusUpdate;
|
2023-05-01 11:34:34 -04:00
|
|
|
|
|
|
|
factory EventTarget.parse(String? text) {
|
|
|
|
if (text == null) return EventTarget.unknown;
|
2023-09-29 09:33:43 -04:00
|
|
|
return EventTarget.values.firstWhere(
|
|
|
|
(element) => element.name.toLowerCase() == text.toLowerCase(),
|
2023-05-01 11:34:34 -04:00
|
|
|
orElse: () => EventTarget.unknown,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-05-06 10:45:26 -04:00
|
|
|
class MessagingClient extends ChangeNotifier {
|
2023-05-16 07:01:42 -04:00
|
|
|
static const Duration _autoRefreshDuration = Duration(seconds: 10);
|
|
|
|
static const Duration _unreadSafeguardDuration = Duration(seconds: 120);
|
|
|
|
static const String _messageBoxKey = "message-box";
|
|
|
|
static const String _lastUpdateKey = "__last-update-time";
|
2023-05-17 07:54:30 -04:00
|
|
|
|
2023-05-02 04:04:54 -04:00
|
|
|
final ApiClient _apiClient;
|
2023-05-06 10:45:26 -04:00
|
|
|
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 = {};
|
2023-05-05 06:40:19 -04:00
|
|
|
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();
|
2023-05-06 11:47:36 -04:00
|
|
|
Friend? selectedFriend;
|
2023-09-30 09:22:37 -04:00
|
|
|
|
2023-05-06 12:34:14 -04:00
|
|
|
Timer? _notifyOnlineTimer;
|
2023-05-06 10:45:26 -04:00
|
|
|
Timer? _autoRefresh;
|
2023-05-16 07:01:42 -04:00
|
|
|
Timer? _unreadSafeguard;
|
2023-05-06 10:57:08 -04:00
|
|
|
String? _initStatus;
|
2023-09-30 09:22:37 -04:00
|
|
|
UserStatus _userStatus = UserStatus.empty().copyWith(onlineStatus: OnlineStatus.online);
|
|
|
|
|
|
|
|
UserStatus get userStatus => _userStatus;
|
2023-05-06 10:45:26 -04:00
|
|
|
|
2023-05-05 05:29:54 -04:00
|
|
|
MessagingClient({required ApiClient apiClient, required NotificationClient notificationClient})
|
2023-09-29 09:33:43 -04:00
|
|
|
: _apiClient = apiClient,
|
|
|
|
_notificationClient = notificationClient {
|
2023-06-03 11:17:54 -04:00
|
|
|
debugPrint("mClient created: $hashCode");
|
2023-05-16 07:01:42 -04:00
|
|
|
Hive.openBox(_messageBoxKey).then((box) async {
|
|
|
|
box.delete(_lastUpdateKey);
|
|
|
|
});
|
2023-09-29 09:33:43 -04:00
|
|
|
_setupHub();
|
2023-05-01 11:34:34 -04:00
|
|
|
}
|
|
|
|
|
2023-05-06 10:45:26 -04:00
|
|
|
@override
|
|
|
|
void dispose() {
|
2023-06-03 11:17:54 -04:00
|
|
|
debugPrint("mClient disposed: $hashCode");
|
2023-05-06 10:45:26 -04:00
|
|
|
_autoRefresh?.cancel();
|
2023-05-06 12:34:14 -04:00
|
|
|
_notifyOnlineTimer?.cancel();
|
2023-05-25 14:19:03 -04:00
|
|
|
_unreadSafeguard?.cancel();
|
2023-09-29 09:33:43 -04:00
|
|
|
_hubManager.dispose();
|
2023-05-06 10:45:26 -04:00
|
|
|
super.dispose();
|
|
|
|
}
|
|
|
|
|
2023-05-17 07:54:30 -04:00
|
|
|
String? get initStatus => _initStatus;
|
2023-05-03 14:03:46 -04:00
|
|
|
|
2023-05-17 07:54:30 -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;
|
2023-05-17 07:54:30 -04:00
|
|
|
|
|
|
|
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-05-06 10:57:08 -04:00
|
|
|
|
2023-09-29 09:33:43 -04:00
|
|
|
Future<void> refreshFriendsListWithErrorHandler() async {
|
2023-05-06 10:45:26 -04:00
|
|
|
try {
|
|
|
|
await refreshFriendsList();
|
|
|
|
} catch (e) {
|
2023-05-06 10:57:08 -04:00
|
|
|
_initStatus = "$e";
|
|
|
|
notifyListeners();
|
2023-05-06 10:45:26 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
Future<void> refreshFriendsList() async {
|
2023-05-16 07:01:42 -04:00
|
|
|
DateTime? lastUpdateUtc = Hive.box(_messageBoxKey).get(_lastUpdateKey);
|
2023-05-06 10:45:26 -04:00
|
|
|
_autoRefresh?.cancel();
|
|
|
|
_autoRefresh = Timer(_autoRefreshDuration, () => refreshFriendsList());
|
|
|
|
|
2023-09-29 07:17:17 -04:00
|
|
|
final friends = await ContactApi.getFriendsList(_apiClient, lastStatusUpdate: lastUpdateUtc);
|
2023-05-04 13:04:33 -04:00
|
|
|
for (final friend in friends) {
|
2023-09-29 09:33:43 -04:00
|
|
|
await _updateContact(friend);
|
2023-05-04 13:04:33 -04:00
|
|
|
}
|
2023-05-16 07:01:42 -04:00
|
|
|
|
2023-05-06 12:34:14 -04:00
|
|
|
_initStatus = "";
|
|
|
|
notifyListeners();
|
|
|
|
}
|
|
|
|
|
2023-09-29 07:17:17 -04:00
|
|
|
void sendMessage(Message message) {
|
2023-05-17 07:54:30 -04:00
|
|
|
final msgBody = message.toMap();
|
2023-09-29 09:33:43 -04:00
|
|
|
_hubManager.send("SendMessage", arguments: [msgBody]);
|
2023-05-17 07:54:30 -04:00
|
|
|
final cache = getUserMessageCache(message.recipientId) ?? _createUserMessageCache(message.recipientId);
|
2023-05-18 07:52:34 -04:00
|
|
|
cache.addMessage(message);
|
2023-05-17 07:54:30 -04:00
|
|
|
notifyListeners();
|
2023-05-04 13:04:33 -04:00
|
|
|
}
|
|
|
|
|
2023-05-17 07:54:30 -04:00
|
|
|
void markMessagesRead(MarkReadBatch batch) {
|
|
|
|
final msgBody = batch.toMap();
|
2023-09-29 09:33:43 -04:00
|
|
|
_hubManager.send("MarkMessagesRead", arguments: [msgBody]);
|
2023-05-17 07:54:30 -04:00
|
|
|
clearUnreadsForUser(batch.senderId);
|
2023-05-05 06:40:19 -04:00
|
|
|
}
|
|
|
|
|
2023-09-30 08:20:08 -04:00
|
|
|
Future<void> setUserStatus(UserStatus status) async {
|
|
|
|
final pkginfo = await PackageInfo.fromPlatform();
|
|
|
|
|
2023-09-30 09:22:37 -04:00
|
|
|
_userStatus = status.copyWith(
|
2023-09-30 08:20:08 -04:00
|
|
|
appVersion: "${pkginfo.version} of ${pkginfo.appName}",
|
|
|
|
isMobile: true,
|
2023-09-30 09:22:37 -04:00
|
|
|
lastStatusChange: DateTime.now(),
|
2023-09-30 08:20:08 -04:00
|
|
|
);
|
|
|
|
|
|
|
|
_hubManager.send("BroadcastStatus", arguments: [
|
2023-09-30 09:22:37 -04:00
|
|
|
_userStatus.toMap(),
|
2023-09-30 08:20:08 -04:00
|
|
|
{
|
|
|
|
"group": 0,
|
|
|
|
"targetIds": [],
|
|
|
|
}
|
|
|
|
]);
|
2023-09-30 09:22:37 -04:00
|
|
|
|
|
|
|
final self = getAsFriend(_apiClient.userId);
|
|
|
|
await _updateContact(self!.copyWith(userStatus: _userStatus));
|
|
|
|
notifyListeners();
|
2023-09-30 08:20:08 -04:00
|
|
|
}
|
|
|
|
|
2023-05-05 06:40:19 -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();
|
2023-05-05 06:40:19 -04:00
|
|
|
_notificationClient.showUnreadMessagesNotification(messages.reversed);
|
2023-05-06 10:45:26 -04:00
|
|
|
notifyListeners();
|
2023-05-05 06:40:19 -04:00
|
|
|
}
|
|
|
|
|
2023-05-17 07:54:30 -04:00
|
|
|
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);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-05-06 13:01:15 -04:00
|
|
|
void clearUnreadsForUser(String userId) {
|
|
|
|
_unreads[userId]?.clear();
|
2023-05-06 10:45:26 -04:00
|
|
|
notifyListeners();
|
2023-05-05 06:40:19 -04:00
|
|
|
}
|
|
|
|
|
2023-05-16 09:57:44 -04:00
|
|
|
void deleteUserMessageCache(String userId) {
|
|
|
|
_messageCache.remove(userId);
|
|
|
|
}
|
|
|
|
|
2023-05-06 11:47:36 -04:00
|
|
|
Future<void> loadUserMessageCache(String userId) async {
|
|
|
|
final cache = getUserMessageCache(userId) ?? _createUserMessageCache(userId);
|
|
|
|
await cache.loadMessages();
|
|
|
|
_messageCache[userId] = cache;
|
|
|
|
notifyListeners();
|
2023-05-01 11:34:34 -04:00
|
|
|
}
|
|
|
|
|
2023-05-17 07:54:30 -04:00
|
|
|
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));
|
2023-05-17 07:54:30 -04:00
|
|
|
notifyListeners();
|
|
|
|
}
|
|
|
|
|
|
|
|
void resetInitStatus() {
|
|
|
|
_initStatus = null;
|
|
|
|
notifyListeners();
|
|
|
|
}
|
|
|
|
|
|
|
|
Future<void> _refreshUnreads() async {
|
|
|
|
try {
|
|
|
|
final unreadMessages = await MessageApi.getUserMessages(_apiClient, unreadOnly: true);
|
|
|
|
updateAllUnreads(unreadMessages.toList());
|
|
|
|
} catch (_) {}
|
|
|
|
}
|
|
|
|
|
|
|
|
void _sortFriendsCache() {
|
|
|
|
_sortedFriendsCache.sort((a, b) {
|
|
|
|
var aVal = friendHasUnreads(a) ? -3 : 0;
|
|
|
|
var bVal = friendHasUnreads(b) ? -3 : 0;
|
|
|
|
|
|
|
|
aVal -= a.latestMessageTime.compareTo(b.latestMessageTime);
|
|
|
|
aVal += a.userStatus.onlineStatus.compareTo(b.userStatus.onlineStatus) * 2;
|
|
|
|
return aVal.compareTo(bVal);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2023-09-29 09:33:43 -04:00
|
|
|
Future<void> _updateContact(Friend friend) async {
|
2023-05-16 07:01:42 -04:00
|
|
|
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);
|
|
|
|
}
|
2023-05-06 13:45:54 -04:00
|
|
|
final sIndex = _sortedFriendsCache.indexWhere((element) => element.id == friend.id);
|
|
|
|
if (sIndex == -1) {
|
|
|
|
_sortedFriendsCache.add(friend);
|
|
|
|
} else {
|
|
|
|
_sortedFriendsCache[sIndex] = friend;
|
|
|
|
}
|
2023-05-29 10:48:53 -04:00
|
|
|
if (friend.id == selectedFriend?.id) {
|
|
|
|
selectedFriend = friend;
|
|
|
|
}
|
2023-05-06 13:45:54 -04:00
|
|
|
_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.");
|
2023-05-01 11:34:34 -04:00
|
|
|
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-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]);
|
|
|
|
},
|
|
|
|
);
|
2023-05-03 11:51:18 -04:00
|
|
|
}
|
2023-05-01 11:34:34 -04:00
|
|
|
|
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-05-01 11:34:34 -04:00
|
|
|
}
|
|
|
|
|
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-05-01 11:34:34 -04:00
|
|
|
}
|
2023-09-29 09:33:43 -04:00
|
|
|
notifyListeners();
|
2023-05-01 11:34:34 -04:00
|
|
|
}
|
|
|
|
|
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-05-01 11:34:34 -04:00
|
|
|
}
|
2023-09-29 09:33:43 -04:00
|
|
|
notifyListeners();
|
2023-05-01 11:34:34 -04:00
|
|
|
}
|
|
|
|
|
2023-09-29 09:33:43 -04:00
|
|
|
void _onReceiveStatusUpdate(List args) {
|
|
|
|
for (final statusUpdate in args) {
|
|
|
|
final status = UserStatus.fromMap(statusUpdate);
|
|
|
|
var friend = getAsFriend(statusUpdate["userId"]);
|
|
|
|
friend = friend?.copyWith(userStatus: status);
|
|
|
|
if (friend != null) {
|
|
|
|
_updateContact(friend);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
notifyListeners();
|
2023-05-03 14:03:46 -04:00
|
|
|
}
|
2023-09-30 06:22:32 -04:00
|
|
|
|
|
|
|
void _onReceiveSessionUpdate(List args) {}
|
2023-09-29 09:33:43 -04:00
|
|
|
}
|