Remove unused code and restructure messaging client a little

This commit is contained in:
Nutcake 2023-05-17 13:54:30 +02:00
parent 856241bd0d
commit 69d69d0aa4
4 changed files with 107 additions and 149 deletions

View file

@ -1,21 +1,20 @@
import 'dart:async'; import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'dart:io'; import 'dart:io';
import 'package:flutter/widgets.dart';
import 'package:hive_flutter/hive_flutter.dart';
import 'package:http/http.dart' as http;
import 'package:logging/logging.dart';
import 'package:contacts_plus_plus/apis/friend_api.dart'; import 'package:contacts_plus_plus/apis/friend_api.dart';
import 'package:contacts_plus_plus/apis/message_api.dart'; import 'package:contacts_plus_plus/apis/message_api.dart';
import 'package:contacts_plus_plus/apis/user_api.dart'; import 'package:contacts_plus_plus/apis/user_api.dart';
import 'package:contacts_plus_plus/clients/notification_client.dart'; import 'package:contacts_plus_plus/clients/notification_client.dart';
import 'package:contacts_plus_plus/models/authentication_data.dart';
import 'package:contacts_plus_plus/models/friend.dart'; import 'package:contacts_plus_plus/models/friend.dart';
import 'package:flutter/widgets.dart';
import 'package:hive_flutter/hive_flutter.dart';
import 'package:http/http.dart' as http;
import 'package:contacts_plus_plus/clients/api_client.dart'; import 'package:contacts_plus_plus/clients/api_client.dart';
import 'package:contacts_plus_plus/config.dart'; import 'package:contacts_plus_plus/config.dart';
import 'package:contacts_plus_plus/models/message.dart'; import 'package:contacts_plus_plus/models/message.dart';
import 'package:logging/logging.dart';
import 'package:workmanager/workmanager.dart';
enum EventType { enum EventType {
unknown, unknown,
@ -43,35 +42,30 @@ enum EventTarget {
} }
class MessagingClient extends ChangeNotifier { class MessagingClient extends ChangeNotifier {
static const String eofChar = ""; static const String _eofChar = "";
static const String _negotiationPacket = "{\"protocol\":\"json\", \"version\":1}$eofChar"; static const String _negotiationPacket = "{\"protocol\":\"json\", \"version\":1}$_eofChar";
static const List<int> _reconnectTimeoutsSeconds = [0, 5, 10, 20, 60]; static const List<int> _reconnectTimeoutsSeconds = [0, 5, 10, 20, 60];
static const String taskName = "periodic-unread-check";
static const Duration _autoRefreshDuration = Duration(seconds: 10); static const Duration _autoRefreshDuration = Duration(seconds: 10);
static const Duration _unreadSafeguardDuration = Duration(seconds: 120); static const Duration _unreadSafeguardDuration = Duration(seconds: 120);
static const String _messageBoxKey = "message-box"; static const String _messageBoxKey = "message-box";
static const String _lastUpdateKey = "__last-update-time"; static const String _lastUpdateKey = "__last-update-time";
final ApiClient _apiClient; final ApiClient _apiClient;
final List<Friend> _sortedFriendsCache = []; // Keep a sorted copy so as to not have to sort during build() final List<Friend> _sortedFriendsCache = []; // Keep a sorted copy so as to not have to sort during build()
final Map<String, MessageCache> _messageCache = {}; final Map<String, MessageCache> _messageCache = {};
final Map<String, List<Message>> _unreads = {}; final Map<String, List<Message>> _unreads = {};
final Logger _logger = Logger("NeosHub"); final Logger _logger = Logger("NeosHub");
final Workmanager _workmanager = Workmanager();
final NotificationClient _notificationClient; final NotificationClient _notificationClient;
Friend? selectedFriend; Friend? selectedFriend;
Timer? _notifyOnlineTimer; Timer? _notifyOnlineTimer;
Timer? _autoRefresh; Timer? _autoRefresh;
Timer? _refreshTimeout;
Timer? _unreadSafeguard; Timer? _unreadSafeguard;
int _attempts = 0; int _attempts = 0;
WebSocket? _wsChannel; WebSocket? _wsChannel;
bool _isConnecting = false; bool _isConnecting = false;
String? _initStatus; String? _initStatus;
String? get initStatus => _initStatus;
bool get websocketConnected => _wsChannel != null;
MessagingClient({required ApiClient apiClient, required NotificationClient notificationClient}) MessagingClient({required ApiClient apiClient, required NotificationClient notificationClient})
: _apiClient = apiClient, _notificationClient = notificationClient { : _apiClient = apiClient, _notificationClient = notificationClient {
Hive.openBox(_messageBoxKey).then((box) async { Hive.openBox(_messageBoxKey).then((box) async {
@ -79,7 +73,7 @@ class MessagingClient extends ChangeNotifier {
await refreshFriendsListWithErrorHandler(); await refreshFriendsListWithErrorHandler();
await _refreshUnreads(); await _refreshUnreads();
}); });
startWebsocket(); _startWebsocket();
_notifyOnlineTimer = Timer.periodic(const Duration(seconds: 60), (timer) async { _notifyOnlineTimer = Timer.periodic(const Duration(seconds: 60), (timer) async {
// We should probably let the MessagingClient handle the entire state of USerStatus instead of mirroring like this // We should probably let the MessagingClient handle the entire state of USerStatus instead of mirroring like this
// but I don't feel like implementing that right now. // but I don't feel like implementing that right now.
@ -90,30 +84,29 @@ class MessagingClient extends ChangeNotifier {
@override @override
void dispose() { void dispose() {
_autoRefresh?.cancel(); _autoRefresh?.cancel();
_refreshTimeout?.cancel();
_notifyOnlineTimer?.cancel(); _notifyOnlineTimer?.cancel();
_wsChannel?.close(); _wsChannel?.close();
super.dispose(); super.dispose();
} }
void _sendData(data) { String? get initStatus => _initStatus;
if (_wsChannel == null) throw "Neos Hub is not connected";
_wsChannel!.add(jsonEncode(data)+eofChar);
}
void resetStatus() { bool get websocketConnected => _wsChannel != null;
_initStatus = null;
notifyListeners(); List<Friend> get cachedFriends => _sortedFriendsCache;
}
List<Message> getUnreadsForFriend(Friend friend) => _unreads[friend.id] ?? [];
bool friendHasUnreads(Friend friend) => _unreads.containsKey(friend.id);
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);
Future<void> _refreshUnreads() async {
_unreadSafeguard?.cancel();
try {
final unreadMessages = await MessageApi.getUserMessages(_apiClient, unreadOnly: true);
updateAllUnreads(unreadMessages.toList());
} catch (_) {}
_unreadSafeguard = Timer(_unreadSafeguardDuration, _refreshUnreads);
}
Future<void> refreshFriendsListWithErrorHandler () async { Future<void> refreshFriendsListWithErrorHandler () async {
try { try {
@ -138,29 +131,32 @@ class MessagingClient extends ChangeNotifier {
notifyListeners(); notifyListeners();
} }
void _sortFriendsCache() { void sendMessage(Message message) async {
_sortedFriendsCache.sort((a, b) { final msgBody = message.toMap();
var aVal = friendHasUnreads(a) ? -3 : 0; final data = {
var bVal = friendHasUnreads(b) ? -3 : 0; "type": EventType.message.index,
"target": "SendMessage",
aVal -= a.latestMessageTime.compareTo(b.latestMessageTime); "arguments": [
aVal += a.userStatus.onlineStatus.compareTo(b.userStatus.onlineStatus) * 2; msgBody
return aVal.compareTo(bVal); ],
}); };
_sendData(data);
final cache = getUserMessageCache(message.recipientId) ?? _createUserMessageCache(message.recipientId);
cache.messages.add(message);
notifyListeners();
} }
void updateAllUnreads(List<Message> messages) { void markMessagesRead(MarkReadBatch batch) {
_unreads.clear(); final msgBody = batch.toMap();
for (final msg in messages) { final data = {
if (msg.senderId != _apiClient.userId) { "type": EventType.message.index,
final value = _unreads[msg.senderId]; "target": "MarkMessagesRead",
if (value == null) { "arguments": [
_unreads[msg.senderId] = [msg]; msgBody
} else { ],
value.add(msg); };
} _sendData(data);
} clearUnreadsForUser(batch.senderId);
}
} }
void addUnread(Message message) { void addUnread(Message message) {
@ -177,25 +173,25 @@ class MessagingClient extends ChangeNotifier {
notifyListeners(); 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) { void clearUnreadsForUser(String userId) {
_unreads[userId]?.clear(); _unreads[userId]?.clear();
notifyListeners(); notifyListeners();
} }
List<Message> getUnreadsForFriend(Friend friend) => _unreads[friend.id] ?? [];
bool friendHasUnreads(Friend friend) => _unreads.containsKey(friend.id);
bool messageIsUnread(Message message) {
return _unreads[message.senderId]?.any((element) => element.id == message.id) ?? false;
}
Friend? getAsFriend(String userId) => Friend.fromMapOrNull(Hive.box(_messageBoxKey).get(userId));
List<Friend> get cachedFriends => _sortedFriendsCache;
MessageCache _createUserMessageCache(String userId) => MessageCache(apiClient: _apiClient, userId: userId);
void deleteUserMessageCache(String userId) { void deleteUserMessageCache(String userId) {
_messageCache.remove(userId); _messageCache.remove(userId);
} }
@ -207,6 +203,39 @@ class MessagingClient extends ChangeNotifier {
notifyListeners(); notifyListeners();
} }
Future<void> updateFriendStatus(String userId) async {
final friend = getAsFriend(userId);
if (friend == null) return;
final newStatus = await UserApi.getUserStatus(_apiClient, userId: userId);
await _updateFriend(friend.copyWith(userStatus: newStatus));
notifyListeners();
}
void resetInitStatus() {
_initStatus = null;
notifyListeners();
}
Future<void> _refreshUnreads() async {
_unreadSafeguard?.cancel();
try {
final unreadMessages = await MessageApi.getUserMessages(_apiClient, unreadOnly: true);
updateAllUnreads(unreadMessages.toList());
} catch (_) {}
_unreadSafeguard = Timer(_unreadSafeguardDuration, _refreshUnreads);
}
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);
});
}
Future<void> _updateFriend(Friend friend) async { Future<void> _updateFriend(Friend friend) async {
final box = Hive.box(_messageBoxKey); final box = Hive.box(_messageBoxKey);
box.put(friend.id, friend.toMap()); box.put(friend.id, friend.toMap());
@ -223,44 +252,15 @@ class MessagingClient extends ChangeNotifier {
_sortFriendsCache(); _sortFriendsCache();
} }
Future<void> updateFriendStatus(String userId) async { // ===== Websocket Stuff =====
final friend = getAsFriend(userId);
if (friend == null) return;
final newStatus = await UserApi.getUserStatus(_apiClient, userId: userId);
await _updateFriend(friend.copyWith(userStatus: newStatus));
notifyListeners();
}
MessageCache? getUserMessageCache(String userId) => _messageCache[userId];
static Future<void> backgroundCheckUnreads(Map<String, dynamic>? inputData) async {
if (inputData == null) return;
final auth = AuthenticationData.fromMap(inputData);
final unreads = await MessageApi.getUserMessages(ApiClient(authenticationData: auth), unreadOnly: true);
for (var message in unreads) {
throw UnimplementedError();
}
}
Future<void> _updateNotificationTask(int minuteInterval) async {
final auth = _apiClient.authenticationData;
if (!auth.isAuthenticated) throw "Unauthenticated";
await _workmanager.cancelByUniqueName(taskName);
_workmanager.registerPeriodicTask(
taskName,
taskName,
frequency: Duration(minutes: minuteInterval),
inputData: auth.toMap(),
);
}
void _onDisconnected(error) async { void _onDisconnected(error) async {
_wsChannel = null; _wsChannel = null;
_logger.warning("Neos Hub connection died with error '$error', reconnecting..."); _logger.warning("Neos Hub connection died with error '$error', reconnecting...");
await startWebsocket(); await _startWebsocket();
} }
Future<void> startWebsocket() async { Future<void> _startWebsocket() async {
if (!_apiClient.isAuthenticated) { if (!_apiClient.isAuthenticated) {
_logger.info("Tried to connect to Neos Hub without authentication, this is probably fine for now."); _logger.info("Tried to connect to Neos Hub without authentication, this is probably fine for now.");
return; return;
@ -311,7 +311,7 @@ class MessagingClient extends ChangeNotifier {
} }
void _handleEvent(event) { void _handleEvent(event) {
final body = jsonDecode((event.toString().replaceAll(eofChar, ""))); final body = jsonDecode((event.toString().replaceAll(_eofChar, "")));
final int rawType = body["type"] ?? 0; final int rawType = body["type"] ?? 0;
if (rawType > EventType.values.length) { if (rawType > EventType.values.length) {
_logger.info("Unhandled event type $rawType: $body"); _logger.info("Unhandled event type $rawType: $body");
@ -378,31 +378,8 @@ class MessagingClient extends ChangeNotifier {
} }
} }
void sendMessage(Message message) async { void _sendData(data) {
final msgBody = message.toMap(); if (_wsChannel == null) throw "Neos Hub is not connected";
final data = { _wsChannel!.add(jsonEncode(data)+_eofChar);
"type": EventType.message.index,
"target": "SendMessage",
"arguments": [
msgBody
],
};
_sendData(data);
final cache = getUserMessageCache(message.recipientId) ?? _createUserMessageCache(message.recipientId);
cache.messages.add(message);
notifyListeners();
}
void markMessagesRead(MarkReadBatch batch) {
final msgBody = batch.toMap();
final data = {
"type": EventType.message.index,
"target": "MarkMessagesRead",
"arguments": [
msgBody
],
};
_sendData(data);
clearUnreadsForUser(batch.senderId);
} }
} }

View file

@ -17,17 +17,11 @@ import 'package:intl/intl.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
import 'package:package_info_plus/package_info_plus.dart'; import 'package:package_info_plus/package_info_plus.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:workmanager/workmanager.dart';
import 'models/authentication_data.dart'; import 'models/authentication_data.dart';
void main() async { void main() async {
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();
if (Platform.isAndroid) {
await Workmanager().initialize(
callbackDispatcher, // The top level function, aka callbackDispatcher
isInDebugMode: true // If enabled it will post a notification whenever the task is running. Handy for debugging tasks
);
}
await Hive.initFlutter(); await Hive.initFlutter();
final dateFormat = DateFormat.Hms(); final dateFormat = DateFormat.Hms();
Logger.root.onRecord.listen((event) => log("${dateFormat.format(event.time)}: ${event.message}", name: event.loggerName, time: event.time)); Logger.root.onRecord.listen((event) => log("${dateFormat.format(event.time)}: ${event.message}", name: event.loggerName, time: event.time));
@ -36,17 +30,6 @@ void main() async {
runApp(Phoenix(child: ContactsPlusPlus(settingsClient: settingsClient,))); runApp(Phoenix(child: ContactsPlusPlus(settingsClient: settingsClient,)));
} }
@pragma('vm:entry-point') // Mandatory if the App is obfuscated or using Flutter 3.1+
void callbackDispatcher() {
Workmanager().executeTask((String task, Map<String, dynamic>? inputData) async {
debugPrint("Native called background task: $task"); //simpleTask will be emitted here.
if (task == MessagingClient.taskName) {
final unreads = MessagingClient.backgroundCheckUnreads(inputData);
}
return Future.value(true);
});
}
class ContactsPlusPlus extends StatefulWidget { class ContactsPlusPlus extends StatefulWidget {
const ContactsPlusPlus({required this.settingsClient, super.key}); const ContactsPlusPlus({required this.settingsClient, super.key});

View file

@ -1,5 +1,3 @@
import 'package:collection/collection.dart';
import 'package:contacts_plus_plus/models/user_profile.dart'; import 'package:contacts_plus_plus/models/user_profile.dart';
class PersonalProfile { class PersonalProfile {

View file

@ -239,7 +239,7 @@ class _FriendsListState extends State<FriendsList> {
child: DefaultErrorWidget( child: DefaultErrorWidget(
message: mClient.initStatus, message: mClient.initStatus,
onRetry: () async { onRetry: () async {
mClient.resetStatus(); mClient.resetInitStatus();
mClient.refreshFriendsListWithErrorHandler(); mClient.refreshFriendsListWithErrorHandler();
}, },
), ),