Fix contact status loading
This commit is contained in:
parent
f305fcd23c
commit
b1f2d65ab8
8 changed files with 262 additions and 192 deletions
|
@ -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));
|
||||
|
|
|
@ -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<int> _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<Friend> _sortedFriendsCache = []; // Keep a sorted copy so as to not have to sort during build()
|
||||
final Map<String, MessageCache> _messageCache = {};
|
||||
final Map<String, List<Message>> _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<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;
|
||||
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<void> refreshFriendsListWithErrorHandler () async {
|
||||
Future<void> 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<void> _updateFriend(Friend friend) async {
|
||||
Future<void> _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<void> _startWebsocket() async {
|
||||
Future<void> _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<WebSocket> _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;
|
||||
}
|
||||
|
||||
}
|
134
lib/hub_manager.dart
Normal file
134
lib/hub_manager.dart
Normal 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();
|
||||
}
|
||||
}
|
|
@ -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" : ""}";
|
||||
|
||||
|
|
|
@ -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(),
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -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<Session> 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,7 +103,7 @@ 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,
|
||||
|
|
|
@ -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(
|
||||
|
|
Loading…
Reference in a new issue