diff --git a/lib/apis/contact_api.dart b/lib/apis/contact_api.dart index a727093..28d849d 100644 --- a/lib/apis/contact_api.dart +++ b/lib/apis/contact_api.dart @@ -22,7 +22,7 @@ class ContactApi { ownerId: client.userId, userStatus: UserStatus.empty(), userProfile: UserProfile.empty(), - friendStatus: FriendStatus.accepted, + contactStatus: FriendStatus.accepted, latestMessageTime: DateTime.now(), ); final body = jsonEncode(friend.toMap(shallow: true)); diff --git a/lib/clients/messaging_client.dart b/lib/clients/messaging_client.dart index 0d1fa2a..81c3657 100644 --- a/lib/clients/messaging_client.dart +++ b/lib/clients/messaging_client.dart @@ -1,7 +1,6 @@ import 'dart:async'; -import 'dart:convert'; -import 'dart:io'; - +import 'package:contacts_plus_plus/hub_manager.dart'; +import 'package:contacts_plus_plus/models/users/user_status.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; import 'package:hive_flutter/hive_flutter.dart'; @@ -13,9 +12,7 @@ import 'package:contacts_plus_plus/apis/user_api.dart'; import 'package:contacts_plus_plus/clients/notification_client.dart'; import 'package:contacts_plus_plus/models/users/friend.dart'; import 'package:contacts_plus_plus/clients/api_client.dart'; -import 'package:contacts_plus_plus/config.dart'; import 'package:contacts_plus_plus/models/message.dart'; -import 'package:uuid/uuid.dart'; enum EventType { undefined, @@ -39,16 +36,14 @@ enum EventTarget { factory EventTarget.parse(String? text) { if (text == null) return EventTarget.unknown; - return EventTarget.values.firstWhere((element) => element.name.toLowerCase() == text.toLowerCase(), + return EventTarget.values.firstWhere( + (element) => element.name.toLowerCase() == text.toLowerCase(), orElse: () => EventTarget.unknown, ); } } class MessagingClient extends ChangeNotifier { - static const String _eofChar = ""; - static const String _negotiationPacket = "{\"protocol\":\"json\", \"version\":1}$_eofChar"; - static const List _reconnectTimeoutsSeconds = [0, 5, 10, 20, 60]; static const Duration _autoRefreshDuration = Duration(seconds: 10); static const Duration _unreadSafeguardDuration = Duration(seconds: 120); static const String _messageBoxKey = "message-box"; @@ -58,28 +53,24 @@ class MessagingClient extends ChangeNotifier { final List _sortedFriendsCache = []; // Keep a sorted copy so as to not have to sort during build() final Map _messageCache = {}; final Map> _unreads = {}; - final Logger _logger = Logger("NeosHub"); + final Logger _logger = Logger("Messaging"); final NotificationClient _notificationClient; + final HubManager _hubManager = HubManager(); Friend? selectedFriend; Timer? _notifyOnlineTimer; Timer? _autoRefresh; Timer? _unreadSafeguard; - int _attempts = 0; - WebSocket? _wsChannel; - bool _isConnecting = false; String? _initStatus; MessagingClient({required ApiClient apiClient, required NotificationClient notificationClient}) - : _apiClient = apiClient, _notificationClient = notificationClient { + : _apiClient = apiClient, + _notificationClient = notificationClient { debugPrint("mClient created: $hashCode"); Hive.openBox(_messageBoxKey).then((box) async { box.delete(_lastUpdateKey); - await refreshFriendsListWithErrorHandler(); - await _refreshUnreads(); - _unreadSafeguard = Timer.periodic(_unreadSafeguardDuration, (timer) => _refreshUnreads()); }); - _startWebsocket(); + _setupHub(); _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 // but I don't feel like implementing that right now. @@ -93,21 +84,20 @@ class MessagingClient extends ChangeNotifier { _autoRefresh?.cancel(); _notifyOnlineTimer?.cancel(); _unreadSafeguard?.cancel(); - _wsChannel?.close(); + _hubManager.dispose(); super.dispose(); } String? get initStatus => _initStatus; - bool get websocketConnected => _wsChannel != null; - List get cachedFriends => _sortedFriendsCache; List 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; + 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)); @@ -115,8 +105,7 @@ class MessagingClient extends ChangeNotifier { MessageCache _createUserMessageCache(String userId) => MessageCache(apiClient: _apiClient, userId: userId); - - Future refreshFriendsListWithErrorHandler () async { + Future refreshFriendsListWithErrorHandler() async { try { await refreshFriendsList(); } catch (e) { @@ -132,7 +121,7 @@ class MessagingClient extends ChangeNotifier { final friends = await ContactApi.getFriendsList(_apiClient, lastStatusUpdate: lastUpdateUtc); for (final friend in friends) { - await _updateFriend(friend); + await _updateContact(friend); } _initStatus = ""; @@ -141,7 +130,7 @@ class MessagingClient extends ChangeNotifier { void sendMessage(Message message) { final msgBody = message.toMap(); - _send("SendMessage", body: msgBody); + _hubManager.send("SendMessage", arguments: [msgBody]); final cache = getUserMessageCache(message.recipientId) ?? _createUserMessageCache(message.recipientId); cache.addMessage(message); notifyListeners(); @@ -149,7 +138,7 @@ class MessagingClient extends ChangeNotifier { void markMessagesRead(MarkReadBatch batch) { final msgBody = batch.toMap(); - _send("MarkMessagesRead", body: msgBody); + _hubManager.send("MarkMessagesRead", arguments: [msgBody]); clearUnreadsForUser(batch.senderId); } @@ -201,7 +190,7 @@ class MessagingClient extends ChangeNotifier { final friend = getAsFriend(userId); if (friend == null) return; final newStatus = await UserApi.getUserStatus(_apiClient, userId: userId); - await _updateFriend(friend.copyWith(userStatus: newStatus)); + await _updateContact(friend.copyWith(userStatus: newStatus)); notifyListeners(); } @@ -228,7 +217,7 @@ class MessagingClient extends ChangeNotifier { }); } - Future _updateFriend(Friend friend) async { + Future _updateContact(Friend friend) async { final box = Hive.box(_messageBoxKey); box.put(friend.id, friend.toMap()); final lastStatusUpdate = box.get(_lastUpdateKey); @@ -247,144 +236,79 @@ class MessagingClient extends ChangeNotifier { _sortFriendsCache(); } - // ===== Websocket Stuff ===== - - void _onDisconnected(error) async { - _wsChannel = null; - _logger.warning("Neos Hub connection died with error '$error', reconnecting..."); - await _startWebsocket(); - } - - Future _startWebsocket() async { + Future _setupHub() async { if (!_apiClient.isAuthenticated) { _logger.info("Tried to connect to Neos Hub without authentication, this is probably fine for now."); return; } - if (_isConnecting) { - return; - } - _isConnecting = true; - _wsChannel = await _tryConnect(); - _isConnecting = false; - _logger.info("Connected to Neos Hub."); - _wsChannel!.done.then((error) => _onDisconnected(error)); - _wsChannel!.listen(_handleEvent, onDone: () => _onDisconnected("Connection closed."), onError: _onDisconnected); - _wsChannel!.add(_negotiationPacket); - _send("InitializeStatus"); + _hubManager.setHeaders(_apiClient.authorizationHeader); + + _hubManager.setHandler(EventTarget.messageSent, _onMessageSent); + _hubManager.setHandler(EventTarget.receiveMessage, _onReceiveMessage); + _hubManager.setHandler(EventTarget.messagesRead, _onMessagesRead); + _hubManager.setHandler(EventTarget.receiveStatusUpdate, _onReceiveStatusUpdate); + + 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]); + }, + ); } - Future _tryConnect() async { - while (true) { - try { - final ws = await WebSocket.connect(Config.resoniteHubUrl.replaceFirst("https://", "wss://"), headers: _apiClient.authorizationHeader); - _attempts = 0; - return ws; - } catch (e) { - final timeout = _reconnectTimeoutsSeconds[_attempts.clamp(0, _reconnectTimeoutsSeconds.length - 1)]; - _logger.severe(e); - _logger.severe("Retrying in $timeout seconds"); - await Future.delayed(Duration(seconds: timeout)); - _attempts++; + 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(); + } + + 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())); + } + notifyListeners(); + } + + 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); + } + notifyListeners(); + } + + 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(); } - - void _handleEvent(event) { - final body = jsonDecode((event.toString().replaceAll(_eofChar, ""))); - final int? rawType = body["type"]; - if (rawType == null) { - _logger.warning("Received empty event, content was $event"); - return; - } - if (rawType > EventType.values.length) { - _logger.info("Unhandled event type $rawType: $body"); - return; - } - switch (EventType.values[rawType]) { - case EventType.streamItem: - case EventType.completion: - case EventType.streamInvocation: - case EventType.cancelInvocation: - case EventType.undefined: - _logger.info("Received unhandled event: $rawType: $body"); - break; - case EventType.invocation: - _logger.info("Received invocation-event."); - _handleInvocation(body); - break; - case EventType.ping: - _logger.info("Received keep-alive."); - break; - case EventType.close: - _logger.severe("Received close-event: ${body["error"]}"); - // Should we trigger a manual reconnect here or just let the remote service close the connection? - break; - } - } - - void _handleInvocation(body) async { - final target = EventTarget.parse(body["target"]); - final args = body["arguments"]; - switch (target) { - case EventTarget.unknown: - _logger.info("Unknown event-target in message: $body"); - return; - case EventTarget.messageSent: - final msg = args[0]; - final message = Message.fromMap(msg, withState: MessageState.sent); - final cache = getUserMessageCache(message.recipientId) ?? _createUserMessageCache(message.recipientId); - cache.addMessage(message); - notifyListeners(); - break; - case EventTarget.receiveMessage: - 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())); - } - notifyListeners(); - break; - case EventTarget.messagesRead: - final messageIds = args[0]["ids"] as List; - final recipientId = args[0]["recipientId"]; - if (recipientId == null) break; - final cache = getUserMessageCache(recipientId); - if (cache == null) break; - for (var id in messageIds) { - cache.setMessageState(id, MessageState.read); - } - notifyListeners(); - break; - case EventTarget.receiveStatusUpdate: - - break; - case EventTarget.removeSession: - case EventTarget.receiveSessionUpdate: - // TODO: Handle session updates - _logger.info("Received unhandled invocation event."); - break; - } - } - - String _send(String target, {Map? body}) { - final invocationId = const Uuid().v4(); - final data = { - "type": EventType.invocation.index, - "invocationId": invocationId, - "target": target, - "arguments": [ - if (body != null) - body - ], - }; - if (_wsChannel == null) throw "Neos Hub is not connected"; - _wsChannel!.add(jsonEncode(data)+_eofChar); - return invocationId; - } - -} \ No newline at end of file +} diff --git a/lib/hub_manager.dart b/lib/hub_manager.dart new file mode 100644 index 0000000..4c0781d --- /dev/null +++ b/lib/hub_manager.dart @@ -0,0 +1,134 @@ +import 'dart:convert'; +import 'dart:io'; + +import 'package:collection/collection.dart'; +import 'package:contacts_plus_plus/clients/messaging_client.dart'; +import 'package:contacts_plus_plus/config.dart'; +import 'package:logging/logging.dart'; +import 'package:uuid/uuid.dart'; + +class HubManager { + static const String _eofChar = ""; + static const String _negotiationPacket = "{\"protocol\":\"json\", \"version\":1}$_eofChar"; + static const List _reconnectTimeoutsSeconds = [0, 5, 10, 20, 60]; + + final Logger _logger = Logger("Hub"); + final Map _headers = {}; + final Map _handlers = {}; + final Map _responseHandlers = {}; + WebSocket? _wsChannel; + bool _isConnecting = false; + int _attempts = 0; + + void setHandler(EventTarget target, Function(List args) function) { + _handlers[target] = function; + } + + void setHeaders(Map headers) { + _headers.addAll(headers); + } + + void _onDisconnected(error) async { + _wsChannel = null; + _logger.warning("Neos Hub connection died with error '$error', reconnecting..."); + await start(); + } + + Future start() async { + if (_isConnecting) { + return; + } + _isConnecting = true; + _wsChannel = await _tryConnect(); + _isConnecting = false; + _logger.info("Connected to Neos Hub."); + _wsChannel!.done.then((error) => _onDisconnected(error)); + _wsChannel!.listen(_handleEvent, onDone: () => _onDisconnected("Connection closed."), onError: _onDisconnected); + _wsChannel!.add(_negotiationPacket); + } + + Future _tryConnect() async { + while (true) { + try { + final ws = await WebSocket.connect(Config.resoniteHubUrl.replaceFirst("https://", "wss://"), headers: _headers); + _attempts = 0; + return ws; + } catch (e) { + final timeout = _reconnectTimeoutsSeconds[_attempts.clamp(0, _reconnectTimeoutsSeconds.length - 1)]; + _logger.severe(e); + _logger.severe("Retrying in $timeout seconds"); + await Future.delayed(Duration(seconds: timeout)); + _attempts++; + } + } + } + + void _handleEvent(event) { + final bodies = event.toString().split(_eofChar); + final eventBodies = bodies.whereNot((element) => element.isEmpty).map((e) => jsonDecode(e)); + for (final body in eventBodies) { + final int? rawType = body["type"]; + if (rawType == null) { + _logger.warning("Received empty event, content was $event"); + continue; + } + if (rawType > EventType.values.length) { + _logger.info("Unhandled event type $rawType: $body"); + continue; + } + switch (EventType.values[rawType]) { + case EventType.streamItem: + case EventType.completion: + final handler = _responseHandlers[body["invocationId"]]; + handler?.call(body["result"] ?? {}); + break; + case EventType.cancelInvocation: + case EventType.undefined: + _logger.info("Received unhandled event: $rawType: $body"); + break; + case EventType.streamInvocation: + case EventType.invocation: + _logger.info("Received invocation-event."); + _handleInvocation(body); + break; + case EventType.ping: + _logger.info("Received keep-alive."); + break; + case EventType.close: + _logger.severe("Received close-event: ${body["error"]}"); + // Should we trigger a manual reconnect here or just let the remote service close the connection? + break; + } + } + } + + void _handleInvocation(body) async { + final target = EventTarget.parse(body["target"]); + final args = body["arguments"] ?? []; + final handler = _handlers[target]; + if (handler == null) { + _logger.info("Unhandled event received"); + return; + } + handler(args); + } + + void send(String target, {List arguments = const [], Function(Map data)? responseHandler}) { + final invocationId = const Uuid().v4(); + final data = { + "type": EventType.invocation.index, + "invocationId": invocationId, + "target": target, + "arguments": arguments, + }; + if (responseHandler != null) { + _responseHandlers[invocationId] = responseHandler; + } + if (_wsChannel == null) throw "Neos Hub is not connected"; + _wsChannel!.add(jsonEncode(data) + _eofChar); + } + + void dispose() { + _wsChannel?.close(); + } +} diff --git a/lib/models/session.dart b/lib/models/session.dart index 4febf70..b501731 100644 --- a/lib/models/session.dart +++ b/lib/models/session.dart @@ -177,7 +177,6 @@ class SessionFilterSettings { String buildRequestString() => "?includeEmptyHeadless=$includeEmptyHeadless" "${"&includeEnded=$includeEnded"}" "${name.isNotEmpty ? "&name=$name" : ""}" - "${!includeIncompatible ? "&compatibilityHash=${Uri.encodeComponent(Config.latestCompatHash)}" : ""}" "${hostName.isNotEmpty ? "&hostName=$hostName" : ""}" "${minActiveUsers > 0 ? "&minActiveUsers=$minActiveUsers" : ""}"; diff --git a/lib/models/users/friend.dart b/lib/models/users/friend.dart index 8e9cdd8..fe20a00 100644 --- a/lib/models/users/friend.dart +++ b/lib/models/users/friend.dart @@ -3,26 +3,27 @@ import 'package:contacts_plus_plus/models/users/user_profile.dart'; import 'package:contacts_plus_plus/models/users/friend_status.dart'; import 'package:contacts_plus_plus/models/users/online_status.dart'; import 'package:contacts_plus_plus/models/users/user_status.dart'; +import 'package:flutter/foundation.dart'; class Friend implements Comparable { static const _emptyId = "-1"; - static const _neosBotId = "U-Neos"; + static const _neosBotId = "U-Resonite"; final String id; final String username; final String ownerId; final UserStatus userStatus; final UserProfile userProfile; - final FriendStatus friendStatus; + final FriendStatus contactStatus; final DateTime latestMessageTime; const Friend({required this.id, required this.username, required this.ownerId, required this.userStatus, required this.userProfile, - required this.friendStatus, required this.latestMessageTime, + required this.contactStatus, required this.latestMessageTime, }); bool get isHeadless => userStatus.activeSessions.any((session) => session.headlessHost == true && session.hostUserId == id); factory Friend.fromMap(Map map) { - final userStatus = map["userStatus"] == null ? UserStatus.empty() : UserStatus.fromMap(map["userStatus"]); + var userStatus = map["userStatus"] == null ? UserStatus.empty() : UserStatus.fromMap(map["userStatus"]); return Friend( id: map["id"], username: map["contactUsername"], @@ -30,7 +31,7 @@ class Friend implements Comparable { // Neos bot status is always offline but should be displayed as online userStatus: map["id"] == _neosBotId ? userStatus.copyWith(onlineStatus: OnlineStatus.online) : userStatus, userProfile: UserProfile.fromMap(map["profile"] ?? {}), - friendStatus: FriendStatus.fromString(map["contactStatus"]), + contactStatus: FriendStatus.fromString(map["contactStatus"]), latestMessageTime: map["latestMessageTime"] == null ? DateTime.fromMillisecondsSinceEpoch(0) : DateTime.parse(map["latestMessageTime"]), ); @@ -48,7 +49,7 @@ class Friend implements Comparable { ownerId: "", userStatus: UserStatus.empty(), userProfile: UserProfile.empty(), - friendStatus: FriendStatus.none, + contactStatus: FriendStatus.none, latestMessageTime: DateTimeX.epoch ); } @@ -64,7 +65,7 @@ class Friend implements Comparable { ownerId: ownerId ?? this.ownerId, userStatus: userStatus ?? this.userStatus, userProfile: userProfile ?? this.userProfile, - friendStatus: friendStatus ?? this.friendStatus, + contactStatus: friendStatus ?? this.contactStatus, latestMessageTime: latestMessageTime ?? this.latestMessageTime, ); } @@ -72,11 +73,11 @@ class Friend implements Comparable { Map toMap({bool shallow=false}) { return { "id": id, - "username": username, + "contactUsername": username, "ownerId": ownerId, "userStatus": userStatus.toMap(shallow: shallow), "profile": userProfile.toMap(), - "friendStatus": friendStatus.name, + "contactStatus": contactStatus.name, "latestMessageTime": latestMessageTime.toIso8601String(), }; } diff --git a/lib/models/users/online_status.dart b/lib/models/users/online_status.dart index 97662b6..0e6de0b 100644 --- a/lib/models/users/online_status.dart +++ b/lib/models/users/online_status.dart @@ -19,7 +19,7 @@ enum OnlineStatus { factory OnlineStatus.fromString(String? text) { return OnlineStatus.values.firstWhere((element) => element.name.toLowerCase() == text?.toLowerCase(), - orElse: () => OnlineStatus.offline, + orElse: () => OnlineStatus.online, ); } diff --git a/lib/models/users/user_status.dart b/lib/models/users/user_status.dart index d3a2176..968bea2 100644 --- a/lib/models/users/user_status.dart +++ b/lib/models/users/user_status.dart @@ -1,3 +1,4 @@ +import 'package:contacts_plus_plus/config.dart'; import 'package:contacts_plus_plus/models/session.dart'; import 'package:contacts_plus_plus/models/users/online_status.dart'; @@ -9,20 +10,26 @@ class UserStatus { final bool currentHosting; final Session currentSession; final List activeSessions; - final String neosVersion; + final String appVersion; final String outputDevice; final bool isMobile; final String compatibilityHash; - const UserStatus( - {required this.onlineStatus, required this.lastStatusChange, required this.currentSession, - required this.currentSessionAccessLevel, required this.currentSessionHidden, required this.currentHosting, - required this.activeSessions, required this.neosVersion, required this.outputDevice, required this.isMobile, - required this.compatibilityHash, - }); + const UserStatus({ + required this.onlineStatus, + required this.lastStatusChange, + required this.currentSession, + required this.currentSessionAccessLevel, + required this.currentSessionHidden, + required this.currentHosting, + required this.activeSessions, + required this.appVersion, + required this.outputDevice, + required this.isMobile, + required this.compatibilityHash, + }); - factory UserStatus.empty() => - UserStatus( + factory UserStatus.empty() => UserStatus( onlineStatus: OnlineStatus.offline, lastStatusChange: DateTime.now(), currentSessionAccessLevel: 0, @@ -30,14 +37,14 @@ class UserStatus { currentHosting: false, currentSession: Session.none(), activeSessions: [], - neosVersion: "", + appVersion: "", outputDevice: "Unknown", isMobile: false, compatibilityHash: "", ); factory UserStatus.fromMap(Map map) { - final statusString = map["onlineStatus"] as String?; + final statusString = map["onlineStatus"].toString(); final status = OnlineStatus.fromString(statusString); return UserStatus( onlineStatus: status, @@ -47,11 +54,10 @@ class UserStatus { currentHosting: map["currentHosting"] ?? false, currentSession: Session.fromMap(map["currentSession"]), activeSessions: (map["activeSessions"] as List? ?? []).map((e) => Session.fromMap(e)).toList(), - neosVersion: map["neosVersion"] ?? "", + appVersion: map["appVersion"] ?? "", outputDevice: map["outputDevice"] ?? "Unknown", isMobile: map["isMobile"] ?? false, - compatibilityHash: map["compatabilityHash"] ?? "" - ); + compatibilityHash: map["compatabilityHash"] ?? ""); } Map toMap({bool shallow = false}) { @@ -62,8 +68,14 @@ class UserStatus { "currentSessionHidden": currentSessionHidden, "currentHosting": currentHosting, "currentSession": currentSession.isNone || shallow ? null : currentSession.toMap(), - "activeSessions": shallow ? [] : activeSessions.map((e) => e.toMap(),).toList(), - "neosVersion": neosVersion, + "activeSessions": shallow + ? [] + : activeSessions + .map( + (e) => e.toMap(), + ) + .toList(), + "neosVersion": appVersion, "outputDevice": outputDevice, "isMobile": isMobile, "compatibilityHash": compatibilityHash, @@ -91,9 +103,9 @@ class UserStatus { currentHosting: currentHosting ?? this.currentHosting, currentSession: currentSession ?? this.currentSession, activeSessions: activeSessions ?? this.activeSessions, - neosVersion: neosVersion ?? this.neosVersion, + appVersion: neosVersion ?? this.appVersion, outputDevice: outputDevice ?? this.outputDevice, isMobile: isMobile ?? this.isMobile, compatibilityHash: compatibilityHash ?? this.compatibilityHash, ); -} \ No newline at end of file +} diff --git a/lib/widgets/friends/friend_online_status_indicator.dart b/lib/widgets/friends/friend_online_status_indicator.dart index 12d2f3a..4899e65 100644 --- a/lib/widgets/friends/friend_online_status_indicator.dart +++ b/lib/widgets/friends/friend_online_status_indicator.dart @@ -9,7 +9,7 @@ class FriendOnlineStatusIndicator extends StatelessWidget { @override Widget build(BuildContext context) { - return userStatus.neosVersion.contains("Contacts++") && userStatus.onlineStatus != OnlineStatus.offline + return userStatus.appVersion.contains("Contacts++") && userStatus.onlineStatus != OnlineStatus.offline ? SizedBox.square( dimension: 10, child: Image.asset(