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,8 +105,7 @@ 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();
} catch (e) { } catch (e) {
@ -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,144 +236,79 @@ 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);
_isConnecting = true; _hubManager.setHandler(EventTarget.receiveMessage, _onReceiveMessage);
_wsChannel = await _tryConnect(); _hubManager.setHandler(EventTarget.messagesRead, _onMessagesRead);
_isConnecting = false; _hubManager.setHandler(EventTarget.receiveStatusUpdate, _onReceiveStatusUpdate);
_logger.info("Connected to Neos Hub.");
_wsChannel!.done.then((error) => _onDisconnected(error)); await _hubManager.start();
_wsChannel!.listen(_handleEvent, onDone: () => _onDisconnected("Connection closed."), onError: _onDisconnected); _hubManager.send(
_wsChannel!.add(_negotiationPacket); "InitializeStatus",
_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<WebSocket> _tryConnect() async { void _onMessageSent(List args) {
while (true) { final msg = args[0];
try { final message = Message.fromMap(msg, withState: MessageState.sent);
final ws = await WebSocket.connect(Config.resoniteHubUrl.replaceFirst("https://", "wss://"), headers: _apiClient.authorizationHeader); final cache = getUserMessageCache(message.recipientId) ?? _createUserMessageCache(message.recipientId);
_attempts = 0; cache.addMessage(message);
return ws; notifyListeners();
} catch (e) { }
final timeout = _reconnectTimeoutsSeconds[_attempts.clamp(0, _reconnectTimeoutsSeconds.length - 1)];
_logger.severe(e); void _onReceiveMessage(List args) {
_logger.severe("Retrying in $timeout seconds"); final msg = args[0];
await Future.delayed(Duration(seconds: timeout)); final message = Message.fromMap(msg);
_attempts++; 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;
}
}

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.compatibilityHash, 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() => 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,9 +103,9 @@ 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(