Fix contact status loading

This commit is contained in:
Nutcake 2023-09-29 15:33:43 +02:00
parent f305fcd23c
commit b1f2d65ab8
8 changed files with 262 additions and 192 deletions

View file

@ -22,7 +22,7 @@ class ContactApi {
ownerId: client.userId, ownerId: client.userId,
userStatus: UserStatus.empty(), userStatus: UserStatus.empty(),
userProfile: UserProfile.empty(), userProfile: UserProfile.empty(),
friendStatus: FriendStatus.accepted, contactStatus: FriendStatus.accepted,
latestMessageTime: DateTime.now(), latestMessageTime: DateTime.now(),
); );
final body = jsonEncode(friend.toMap(shallow: true)); final body = jsonEncode(friend.toMap(shallow: true));

View file

@ -1,7 +1,6 @@
import 'dart:async'; import 'dart:async';
import 'dart:convert'; import 'package:contacts_plus_plus/hub_manager.dart';
import 'dart:io'; import 'package:contacts_plus_plus/models/users/user_status.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:hive_flutter/hive_flutter.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/clients/notification_client.dart';
import 'package:contacts_plus_plus/models/users/friend.dart'; import 'package:contacts_plus_plus/models/users/friend.dart';
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/models/message.dart'; import 'package:contacts_plus_plus/models/message.dart';
import 'package:uuid/uuid.dart';
enum EventType { enum EventType {
undefined, undefined,
@ -39,16 +36,14 @@ enum EventTarget {
factory EventTarget.parse(String? text) { factory EventTarget.parse(String? text) {
if (text == null) return EventTarget.unknown; 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, orElse: () => EventTarget.unknown,
); );
} }
} }
class MessagingClient extends ChangeNotifier { class MessagingClient extends ChangeNotifier {
static const String _eofChar = "";
static const String _negotiationPacket = "{\"protocol\":\"json\", \"version\":1}$_eofChar";
static const List<int> _reconnectTimeoutsSeconds = [0, 5, 10, 20, 60];
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";
@ -58,28 +53,24 @@ class MessagingClient extends ChangeNotifier {
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("Messaging");
final NotificationClient _notificationClient; final NotificationClient _notificationClient;
final HubManager _hubManager = HubManager();
Friend? selectedFriend; Friend? selectedFriend;
Timer? _notifyOnlineTimer; Timer? _notifyOnlineTimer;
Timer? _autoRefresh; Timer? _autoRefresh;
Timer? _unreadSafeguard; Timer? _unreadSafeguard;
int _attempts = 0;
WebSocket? _wsChannel;
bool _isConnecting = false;
String? _initStatus; String? _initStatus;
MessagingClient({required ApiClient apiClient, required NotificationClient notificationClient}) MessagingClient({required ApiClient apiClient, required NotificationClient notificationClient})
: _apiClient = apiClient, _notificationClient = notificationClient { : _apiClient = apiClient,
_notificationClient = notificationClient {
debugPrint("mClient created: $hashCode"); debugPrint("mClient created: $hashCode");
Hive.openBox(_messageBoxKey).then((box) async { Hive.openBox(_messageBoxKey).then((box) async {
box.delete(_lastUpdateKey); box.delete(_lastUpdateKey);
await refreshFriendsListWithErrorHandler();
await _refreshUnreads();
_unreadSafeguard = Timer.periodic(_unreadSafeguardDuration, (timer) => _refreshUnreads());
}); });
_startWebsocket(); _setupHub();
_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.
@ -93,21 +84,20 @@ class MessagingClient extends ChangeNotifier {
_autoRefresh?.cancel(); _autoRefresh?.cancel();
_notifyOnlineTimer?.cancel(); _notifyOnlineTimer?.cancel();
_unreadSafeguard?.cancel(); _unreadSafeguard?.cancel();
_wsChannel?.close(); _hubManager.dispose();
super.dispose(); super.dispose();
} }
String? get initStatus => _initStatus; String? get initStatus => _initStatus;
bool get websocketConnected => _wsChannel != null;
List<Friend> get cachedFriends => _sortedFriendsCache; List<Friend> get cachedFriends => _sortedFriendsCache;
List<Message> getUnreadsForFriend(Friend friend) => _unreads[friend.id] ?? []; List<Message> getUnreadsForFriend(Friend friend) => _unreads[friend.id] ?? [];
bool friendHasUnreads(Friend friend) => _unreads.containsKey(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)); Friend? getAsFriend(String userId) => Friend.fromMapOrNull(Hive.box(_messageBoxKey).get(userId));
@ -115,7 +105,6 @@ class MessagingClient extends ChangeNotifier {
MessageCache _createUserMessageCache(String userId) => MessageCache(apiClient: _apiClient, userId: userId); MessageCache _createUserMessageCache(String userId) => MessageCache(apiClient: _apiClient, userId: userId);
Future<void> refreshFriendsListWithErrorHandler() async { Future<void> refreshFriendsListWithErrorHandler() async {
try { try {
await refreshFriendsList(); await refreshFriendsList();
@ -132,7 +121,7 @@ class MessagingClient extends ChangeNotifier {
final friends = await ContactApi.getFriendsList(_apiClient, lastStatusUpdate: lastUpdateUtc); final friends = await ContactApi.getFriendsList(_apiClient, lastStatusUpdate: lastUpdateUtc);
for (final friend in friends) { for (final friend in friends) {
await _updateFriend(friend); await _updateContact(friend);
} }
_initStatus = ""; _initStatus = "";
@ -141,7 +130,7 @@ class MessagingClient extends ChangeNotifier {
void sendMessage(Message message) { void sendMessage(Message message) {
final msgBody = message.toMap(); final msgBody = message.toMap();
_send("SendMessage", body: msgBody); _hubManager.send("SendMessage", arguments: [msgBody]);
final cache = getUserMessageCache(message.recipientId) ?? _createUserMessageCache(message.recipientId); final cache = getUserMessageCache(message.recipientId) ?? _createUserMessageCache(message.recipientId);
cache.addMessage(message); cache.addMessage(message);
notifyListeners(); notifyListeners();
@ -149,7 +138,7 @@ class MessagingClient extends ChangeNotifier {
void markMessagesRead(MarkReadBatch batch) { void markMessagesRead(MarkReadBatch batch) {
final msgBody = batch.toMap(); final msgBody = batch.toMap();
_send("MarkMessagesRead", body: msgBody); _hubManager.send("MarkMessagesRead", arguments: [msgBody]);
clearUnreadsForUser(batch.senderId); clearUnreadsForUser(batch.senderId);
} }
@ -201,7 +190,7 @@ class MessagingClient extends ChangeNotifier {
final friend = getAsFriend(userId); final friend = getAsFriend(userId);
if (friend == null) return; if (friend == null) return;
final newStatus = await UserApi.getUserStatus(_apiClient, userId: userId); final newStatus = await UserApi.getUserStatus(_apiClient, userId: userId);
await _updateFriend(friend.copyWith(userStatus: newStatus)); await _updateContact(friend.copyWith(userStatus: newStatus));
notifyListeners(); notifyListeners();
} }
@ -228,7 +217,7 @@ class MessagingClient extends ChangeNotifier {
}); });
} }
Future<void> _updateFriend(Friend friend) async { Future<void> _updateContact(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());
final lastStatusUpdate = box.get(_lastUpdateKey); final lastStatusUpdate = box.get(_lastUpdateKey);
@ -247,96 +236,45 @@ class MessagingClient extends ChangeNotifier {
_sortFriendsCache(); _sortFriendsCache();
} }
// ===== Websocket Stuff ===== Future<void> _setupHub() async {
void _onDisconnected(error) async {
_wsChannel = null;
_logger.warning("Neos Hub connection died with error '$error', reconnecting...");
await _startWebsocket();
}
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;
} }
if (_isConnecting) { _hubManager.setHeaders(_apiClient.authorizationHeader);
return;
_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);
} }
_isConnecting = true; _initStatus = "";
_wsChannel = await _tryConnect(); notifyListeners();
_isConnecting = false; await _refreshUnreads();
_logger.info("Connected to Neos Hub."); _unreadSafeguard = Timer.periodic(_unreadSafeguardDuration, (timer) => _refreshUnreads());
_wsChannel!.done.then((error) => _onDisconnected(error)); _hubManager.send("RequestStatus", arguments: [null, false]);
_wsChannel!.listen(_handleEvent, onDone: () => _onDisconnected("Connection closed."), onError: _onDisconnected); },
_wsChannel!.add(_negotiationPacket); );
_send("InitializeStatus");
} }
Future<WebSocket> _tryConnect() async { void _onMessageSent(List args) {
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 _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 msg = args[0];
final message = Message.fromMap(msg, withState: MessageState.sent); final message = Message.fromMap(msg, withState: MessageState.sent);
final cache = getUserMessageCache(message.recipientId) ?? _createUserMessageCache(message.recipientId); final cache = getUserMessageCache(message.recipientId) ?? _createUserMessageCache(message.recipientId);
cache.addMessage(message); cache.addMessage(message);
notifyListeners(); notifyListeners();
break; }
case EventTarget.receiveMessage:
void _onReceiveMessage(List args) {
final msg = args[0]; final msg = args[0];
final message = Message.fromMap(msg); final message = Message.fromMap(msg);
final cache = getUserMessageCache(message.senderId) ?? _createUserMessageCache(message.senderId); final cache = getUserMessageCache(message.senderId) ?? _createUserMessageCache(message.senderId);
@ -348,43 +286,29 @@ class MessagingClient extends ChangeNotifier {
markMessagesRead(MarkReadBatch(senderId: message.senderId, ids: [message.id], readTime: DateTime.now())); markMessagesRead(MarkReadBatch(senderId: message.senderId, ids: [message.id], readTime: DateTime.now()));
} }
notifyListeners(); notifyListeners();
break; }
case EventTarget.messagesRead:
void _onMessagesRead(List args) {
final messageIds = args[0]["ids"] as List; final messageIds = args[0]["ids"] as List;
final recipientId = args[0]["recipientId"]; final recipientId = args[0]["recipientId"];
if (recipientId == null) break; if (recipientId == null) return;
final cache = getUserMessageCache(recipientId); final cache = getUserMessageCache(recipientId);
if (cache == null) break; if (cache == null) return;
for (var id in messageIds) { for (var id in messageIds) {
cache.setMessageState(id, MessageState.read); cache.setMessageState(id, MessageState.read);
} }
notifyListeners(); 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}) { void _onReceiveStatusUpdate(List args) {
final invocationId = const Uuid().v4(); for (final statusUpdate in args) {
final data = { final status = UserStatus.fromMap(statusUpdate);
"type": EventType.invocation.index, var friend = getAsFriend(statusUpdate["userId"]);
"invocationId": invocationId, friend = friend?.copyWith(userStatus: status);
"target": target, if (friend != null) {
"arguments": [ _updateContact(friend);
if (body != null) }
body }
], notifyListeners();
};
if (_wsChannel == null) throw "Neos Hub is not connected";
_wsChannel!.add(jsonEncode(data)+_eofChar);
return invocationId;
} }
} }

134
lib/hub_manager.dart Normal file
View file

@ -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<int> _reconnectTimeoutsSeconds = [0, 5, 10, 20, 60];
final Logger _logger = Logger("Hub");
final Map<String, dynamic> _headers = {};
final Map<EventTarget, dynamic Function(List arguments)> _handlers = {};
final Map<String, dynamic Function(Map result)> _responseHandlers = {};
WebSocket? _wsChannel;
bool _isConnecting = false;
int _attempts = 0;
void setHandler(EventTarget target, Function(List args) function) {
_handlers[target] = function;
}
void setHeaders(Map<String, dynamic> headers) {
_headers.addAll(headers);
}
void _onDisconnected(error) async {
_wsChannel = null;
_logger.warning("Neos Hub connection died with error '$error', reconnecting...");
await start();
}
Future<void> 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<WebSocket> _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();
}
}

View file

@ -177,7 +177,6 @@ class SessionFilterSettings {
String buildRequestString() => "?includeEmptyHeadless=$includeEmptyHeadless" String buildRequestString() => "?includeEmptyHeadless=$includeEmptyHeadless"
"${"&includeEnded=$includeEnded"}" "${"&includeEnded=$includeEnded"}"
"${name.isNotEmpty ? "&name=$name" : ""}" "${name.isNotEmpty ? "&name=$name" : ""}"
"${!includeIncompatible ? "&compatibilityHash=${Uri.encodeComponent(Config.latestCompatHash)}" : ""}"
"${hostName.isNotEmpty ? "&hostName=$hostName" : ""}" "${hostName.isNotEmpty ? "&hostName=$hostName" : ""}"
"${minActiveUsers > 0 ? "&minActiveUsers=$minActiveUsers" : ""}"; "${minActiveUsers > 0 ? "&minActiveUsers=$minActiveUsers" : ""}";

View file

@ -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/friend_status.dart';
import 'package:contacts_plus_plus/models/users/online_status.dart'; import 'package:contacts_plus_plus/models/users/online_status.dart';
import 'package:contacts_plus_plus/models/users/user_status.dart'; import 'package:contacts_plus_plus/models/users/user_status.dart';
import 'package:flutter/foundation.dart';
class Friend implements Comparable { class Friend implements Comparable {
static const _emptyId = "-1"; static const _emptyId = "-1";
static const _neosBotId = "U-Neos"; static const _neosBotId = "U-Resonite";
final String id; final String id;
final String username; final String username;
final String ownerId; final String ownerId;
final UserStatus userStatus; final UserStatus userStatus;
final UserProfile userProfile; final UserProfile userProfile;
final FriendStatus friendStatus; final FriendStatus contactStatus;
final DateTime latestMessageTime; final DateTime latestMessageTime;
const Friend({required this.id, required this.username, required this.ownerId, required this.userStatus, required this.userProfile, 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); bool get isHeadless => userStatus.activeSessions.any((session) => session.headlessHost == true && session.hostUserId == id);
factory Friend.fromMap(Map map) { 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( return Friend(
id: map["id"], id: map["id"],
username: map["contactUsername"], username: map["contactUsername"],
@ -30,7 +31,7 @@ class Friend implements Comparable {
// Neos bot status is always offline but should be displayed as online // Neos bot status is always offline but should be displayed as online
userStatus: map["id"] == _neosBotId ? userStatus.copyWith(onlineStatus: OnlineStatus.online) : userStatus, userStatus: map["id"] == _neosBotId ? userStatus.copyWith(onlineStatus: OnlineStatus.online) : userStatus,
userProfile: UserProfile.fromMap(map["profile"] ?? {}), userProfile: UserProfile.fromMap(map["profile"] ?? {}),
friendStatus: FriendStatus.fromString(map["contactStatus"]), contactStatus: FriendStatus.fromString(map["contactStatus"]),
latestMessageTime: map["latestMessageTime"] == null latestMessageTime: map["latestMessageTime"] == null
? DateTime.fromMillisecondsSinceEpoch(0) : DateTime.parse(map["latestMessageTime"]), ? DateTime.fromMillisecondsSinceEpoch(0) : DateTime.parse(map["latestMessageTime"]),
); );
@ -48,7 +49,7 @@ class Friend implements Comparable {
ownerId: "", ownerId: "",
userStatus: UserStatus.empty(), userStatus: UserStatus.empty(),
userProfile: UserProfile.empty(), userProfile: UserProfile.empty(),
friendStatus: FriendStatus.none, contactStatus: FriendStatus.none,
latestMessageTime: DateTimeX.epoch latestMessageTime: DateTimeX.epoch
); );
} }
@ -64,7 +65,7 @@ class Friend implements Comparable {
ownerId: ownerId ?? this.ownerId, ownerId: ownerId ?? this.ownerId,
userStatus: userStatus ?? this.userStatus, userStatus: userStatus ?? this.userStatus,
userProfile: userProfile ?? this.userProfile, userProfile: userProfile ?? this.userProfile,
friendStatus: friendStatus ?? this.friendStatus, contactStatus: friendStatus ?? this.contactStatus,
latestMessageTime: latestMessageTime ?? this.latestMessageTime, latestMessageTime: latestMessageTime ?? this.latestMessageTime,
); );
} }
@ -72,11 +73,11 @@ class Friend implements Comparable {
Map toMap({bool shallow=false}) { Map toMap({bool shallow=false}) {
return { return {
"id": id, "id": id,
"username": username, "contactUsername": username,
"ownerId": ownerId, "ownerId": ownerId,
"userStatus": userStatus.toMap(shallow: shallow), "userStatus": userStatus.toMap(shallow: shallow),
"profile": userProfile.toMap(), "profile": userProfile.toMap(),
"friendStatus": friendStatus.name, "contactStatus": contactStatus.name,
"latestMessageTime": latestMessageTime.toIso8601String(), "latestMessageTime": latestMessageTime.toIso8601String(),
}; };
} }

View file

@ -19,7 +19,7 @@ enum OnlineStatus {
factory OnlineStatus.fromString(String? text) { factory OnlineStatus.fromString(String? text) {
return OnlineStatus.values.firstWhere((element) => element.name.toLowerCase() == text?.toLowerCase(), return OnlineStatus.values.firstWhere((element) => element.name.toLowerCase() == text?.toLowerCase(),
orElse: () => OnlineStatus.offline, orElse: () => OnlineStatus.online,
); );
} }

View file

@ -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/session.dart';
import 'package:contacts_plus_plus/models/users/online_status.dart'; import 'package:contacts_plus_plus/models/users/online_status.dart';
@ -9,20 +10,26 @@ class UserStatus {
final bool currentHosting; final bool currentHosting;
final Session currentSession; final Session currentSession;
final List<Session> activeSessions; final List<Session> activeSessions;
final String neosVersion; final String appVersion;
final String outputDevice; final String outputDevice;
final bool isMobile; final bool isMobile;
final String compatibilityHash; final String compatibilityHash;
const UserStatus( const UserStatus({
{required this.onlineStatus, required this.lastStatusChange, required this.currentSession, required this.onlineStatus,
required this.currentSessionAccessLevel, required this.currentSessionHidden, required this.currentHosting, required this.lastStatusChange,
required this.activeSessions, required this.neosVersion, required this.outputDevice, required this.isMobile, 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, required this.compatibilityHash,
}); });
factory UserStatus.empty() => factory UserStatus.empty() => UserStatus(
UserStatus(
onlineStatus: OnlineStatus.offline, onlineStatus: OnlineStatus.offline,
lastStatusChange: DateTime.now(), lastStatusChange: DateTime.now(),
currentSessionAccessLevel: 0, currentSessionAccessLevel: 0,
@ -30,14 +37,14 @@ class UserStatus {
currentHosting: false, currentHosting: false,
currentSession: Session.none(), currentSession: Session.none(),
activeSessions: [], activeSessions: [],
neosVersion: "", appVersion: "",
outputDevice: "Unknown", outputDevice: "Unknown",
isMobile: false, isMobile: false,
compatibilityHash: "", compatibilityHash: "",
); );
factory UserStatus.fromMap(Map map) { factory UserStatus.fromMap(Map map) {
final statusString = map["onlineStatus"] as String?; final statusString = map["onlineStatus"].toString();
final status = OnlineStatus.fromString(statusString); final status = OnlineStatus.fromString(statusString);
return UserStatus( return UserStatus(
onlineStatus: status, onlineStatus: status,
@ -47,11 +54,10 @@ class UserStatus {
currentHosting: map["currentHosting"] ?? false, currentHosting: map["currentHosting"] ?? false,
currentSession: Session.fromMap(map["currentSession"]), currentSession: Session.fromMap(map["currentSession"]),
activeSessions: (map["activeSessions"] as List? ?? []).map((e) => Session.fromMap(e)).toList(), activeSessions: (map["activeSessions"] as List? ?? []).map((e) => Session.fromMap(e)).toList(),
neosVersion: map["neosVersion"] ?? "", appVersion: map["appVersion"] ?? "",
outputDevice: map["outputDevice"] ?? "Unknown", outputDevice: map["outputDevice"] ?? "Unknown",
isMobile: map["isMobile"] ?? false, isMobile: map["isMobile"] ?? false,
compatibilityHash: map["compatabilityHash"] ?? "" compatibilityHash: map["compatabilityHash"] ?? "");
);
} }
Map toMap({bool shallow = false}) { Map toMap({bool shallow = false}) {
@ -62,8 +68,14 @@ class UserStatus {
"currentSessionHidden": currentSessionHidden, "currentSessionHidden": currentSessionHidden,
"currentHosting": currentHosting, "currentHosting": currentHosting,
"currentSession": currentSession.isNone || shallow ? null : currentSession.toMap(), "currentSession": currentSession.isNone || shallow ? null : currentSession.toMap(),
"activeSessions": shallow ? [] : activeSessions.map((e) => e.toMap(),).toList(), "activeSessions": shallow
"neosVersion": neosVersion, ? []
: activeSessions
.map(
(e) => e.toMap(),
)
.toList(),
"neosVersion": appVersion,
"outputDevice": outputDevice, "outputDevice": outputDevice,
"isMobile": isMobile, "isMobile": isMobile,
"compatibilityHash": compatibilityHash, "compatibilityHash": compatibilityHash,
@ -91,7 +103,7 @@ class UserStatus {
currentHosting: currentHosting ?? this.currentHosting, currentHosting: currentHosting ?? this.currentHosting,
currentSession: currentSession ?? this.currentSession, currentSession: currentSession ?? this.currentSession,
activeSessions: activeSessions ?? this.activeSessions, activeSessions: activeSessions ?? this.activeSessions,
neosVersion: neosVersion ?? this.neosVersion, appVersion: neosVersion ?? this.appVersion,
outputDevice: outputDevice ?? this.outputDevice, outputDevice: outputDevice ?? this.outputDevice,
isMobile: isMobile ?? this.isMobile, isMobile: isMobile ?? this.isMobile,
compatibilityHash: compatibilityHash ?? this.compatibilityHash, compatibilityHash: compatibilityHash ?? this.compatibilityHash,

View file

@ -9,7 +9,7 @@ class FriendOnlineStatusIndicator extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return userStatus.neosVersion.contains("Contacts++") && userStatus.onlineStatus != OnlineStatus.offline return userStatus.appVersion.contains("Contacts++") && userStatus.onlineStatus != OnlineStatus.offline
? SizedBox.square( ? SizedBox.square(
dimension: 10, dimension: 10,
child: Image.asset( child: Image.asset(