Fix user session status not showing

This commit is contained in:
Nutcake 2023-10-03 18:20:02 +02:00
parent 56ed403d79
commit bace94b6d2
9 changed files with 130 additions and 100 deletions

View file

@ -1,51 +1,23 @@
import 'dart:async'; import 'dart:async';
import 'package:contacts_plus_plus/apis/session_api.dart';
import 'package:contacts_plus_plus/apis/contact_api.dart';
import 'package:contacts_plus_plus/apis/message_api.dart';
import 'package:contacts_plus_plus/apis/user_api.dart';
import 'package:contacts_plus_plus/clients/api_client.dart';
import 'package:contacts_plus_plus/clients/notification_client.dart';
import 'package:contacts_plus_plus/crypto_helper.dart'; import 'package:contacts_plus_plus/crypto_helper.dart';
import 'package:contacts_plus_plus/hub_manager.dart'; import 'package:contacts_plus_plus/hub_manager.dart';
import 'package:contacts_plus_plus/models/hub_events.dart';
import 'package:contacts_plus_plus/models/message.dart';
import 'package:contacts_plus_plus/models/session.dart'; import 'package:contacts_plus_plus/models/session.dart';
import 'package:contacts_plus_plus/models/users/friend.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'; 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';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
import 'package:contacts_plus_plus/apis/contact_api.dart';
import 'package:contacts_plus_plus/apis/message_api.dart';
import 'package:contacts_plus_plus/apis/user_api.dart';
import 'package:contacts_plus_plus/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/models/message.dart';
import 'package:package_info_plus/package_info_plus.dart'; import 'package:package_info_plus/package_info_plus.dart';
enum EventType {
undefined,
invocation,
streamItem,
completion,
streamInvocation,
cancelInvocation,
ping,
close;
}
enum EventTarget {
unknown,
messageSent,
receiveMessage,
messagesRead,
receiveSessionUpdate,
removeSession,
receiveStatusUpdate;
factory EventTarget.parse(String? text) {
if (text == null) return EventTarget.unknown;
return EventTarget.values.firstWhere(
(element) => element.name.toLowerCase() == text.toLowerCase(),
orElse: () => EventTarget.unknown,
);
}
}
class MessagingClient extends ChangeNotifier { class MessagingClient extends ChangeNotifier {
static const Duration _autoRefreshDuration = Duration(seconds: 10); static const Duration _autoRefreshDuration = Duration(seconds: 10);
@ -77,11 +49,6 @@ class MessagingClient extends ChangeNotifier {
debugPrint("mClient created: $hashCode"); debugPrint("mClient created: $hashCode");
Hive.openBox(_messageBoxKey).then((box) async { Hive.openBox(_messageBoxKey).then((box) async {
await box.delete(_lastUpdateKey); await box.delete(_lastUpdateKey);
final activeSessions = await SessionApi.getSessions(apiClient);
for (final session in activeSessions) {
final idHash = CryptoHelper.idHash(session.id + _userStatus.hashSalt);
_sessionMap[idHash] = session;
}
_setupHub(); _setupHub();
}); });
} }
@ -113,8 +80,6 @@ class MessagingClient extends ChangeNotifier {
MessageCache _createUserMessageCache(String userId) => MessageCache(apiClient: _apiClient, userId: userId); MessageCache _createUserMessageCache(String userId) => MessageCache(apiClient: _apiClient, userId: userId);
Session? getSessionInfo(String idHash) => _sessionMap[idHash];
Future<void> refreshFriendsListWithErrorHandler() async { Future<void> refreshFriendsListWithErrorHandler() async {
try { try {
await refreshFriendsList(); await refreshFriendsList();
@ -155,9 +120,10 @@ class MessagingClient extends ChangeNotifier {
Future<void> setUserStatus(UserStatus status) async { Future<void> setUserStatus(UserStatus status) async {
final pkginfo = await PackageInfo.fromPlatform(); final pkginfo = await PackageInfo.fromPlatform();
_userStatus = status.copyWith( _userStatus = _userStatus.copyWith(
appVersion: "${pkginfo.version} of ${pkginfo.appName}", appVersion: "${pkginfo.version} of ${pkginfo.appName}",
lastStatusChange: DateTime.now(), lastStatusChange: DateTime.now(),
onlineStatus: status.onlineStatus,
); );
_hubManager.send( _hubManager.send(
@ -165,14 +131,16 @@ class MessagingClient extends ChangeNotifier {
arguments: [ arguments: [
_userStatus.toMap(), _userStatus.toMap(),
{ {
"group": 0, "group": 1,
"targetIds": [], "targetIds": null,
} }
], ],
); );
final self = getAsFriend(_apiClient.userId); final self = getAsFriend(_apiClient.userId);
await _updateContact(self!.copyWith(userStatus: _userStatus)); if (self != null) {
await _updateContact(self.copyWith(userStatus: _userStatus));
}
notifyListeners(); notifyListeners();
} }
@ -284,7 +252,7 @@ class MessagingClient extends ChangeNotifier {
_hubManager.setHandler(EventTarget.receiveSessionUpdate, _onReceiveSessionUpdate); _hubManager.setHandler(EventTarget.receiveSessionUpdate, _onReceiveSessionUpdate);
await _hubManager.start(); await _hubManager.start();
setUserStatus(userStatus); await setUserStatus(userStatus);
_hubManager.send( _hubManager.send(
"InitializeStatus", "InitializeStatus",
responseHandler: (Map data) async { responseHandler: (Map data) async {
@ -302,6 +270,10 @@ class MessagingClient extends ChangeNotifier {
); );
} }
Map<String, Session> createSessionMap(String salt) {
return _sessionMap.map((key, value) => MapEntry(CryptoHelper.idHash(value.id + salt), value));
}
void _onMessageSent(List args) { void _onMessageSent(List args) {
final msg = args[0]; final msg = args[0];
final message = Message.fromMap(msg, withState: MessageState.sent); final message = Message.fromMap(msg, withState: MessageState.sent);
@ -338,9 +310,11 @@ class MessagingClient extends ChangeNotifier {
void _onReceiveStatusUpdate(List args) { void _onReceiveStatusUpdate(List args) {
for (final statusUpdate in args) { for (final statusUpdate in args) {
final status = UserStatus.fromMap(statusUpdate); var status = UserStatus.fromMap(statusUpdate);
var friend = getAsFriend(statusUpdate["userId"]); final sessionMap = createSessionMap(status.hashSalt);
friend = friend?.copyWith(userStatus: status); status = status.copyWith(
sessionData: status.sessions.map((e) => sessionMap[e.sessionHash] ?? Session.none()).toList());
final friend = getAsFriend(statusUpdate["userId"])?.copyWith(userStatus: status);
if (friend != null) { if (friend != null) {
_updateContact(friend); _updateContact(friend);
} }
@ -351,8 +325,7 @@ class MessagingClient extends ChangeNotifier {
void _onReceiveSessionUpdate(List args) { void _onReceiveSessionUpdate(List args) {
for (final sessionUpdate in args) { for (final sessionUpdate in args) {
final session = Session.fromMap(sessionUpdate); final session = Session.fromMap(sessionUpdate);
final idHash = CryptoHelper.idHash(session.id + _userStatus.hashSalt); _sessionMap[session.id] = session;
_sessionMap[idHash] = session;
} }
notifyListeners(); notifyListeners();
} }

View file

@ -5,5 +5,5 @@ class Config {
static const int messageCacheValiditySeconds = 90; static const int messageCacheValiditySeconds = 90;
static const String latestCompatHash = "jnnkdwkBqGv5+jlf1u/k7A=="; static const String latestCompatHash = "YPDxN4N9fu7ZgV+Nr/AHQw==";
} }

View file

@ -2,8 +2,8 @@ import 'dart:convert';
import 'dart:io'; import 'dart:io';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:contacts_plus_plus/clients/messaging_client.dart';
import 'package:contacts_plus_plus/config.dart'; import 'package:contacts_plus_plus/config.dart';
import 'package:contacts_plus_plus/models/hub_events.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
import 'package:uuid/uuid.dart'; import 'package:uuid/uuid.dart';
@ -81,6 +81,7 @@ class HubManager {
case EventType.completion: case EventType.completion:
final handler = _responseHandlers[body["invocationId"]]; final handler = _responseHandlers[body["invocationId"]];
handler?.call(body["result"] ?? {}); handler?.call(body["result"] ?? {});
_logger.info("Received completion event: $rawType: $body");
break; break;
case EventType.cancelInvocation: case EventType.cancelInvocation:
case EventType.undefined: case EventType.undefined:

View file

@ -0,0 +1,28 @@
enum EventType {
undefined,
invocation,
streamItem,
completion,
streamInvocation,
cancelInvocation,
ping,
close;
}
enum EventTarget {
unknown,
messageSent,
receiveMessage,
messagesRead,
receiveSessionUpdate,
removeSession,
receiveStatusUpdate;
factory EventTarget.parse(String? text) {
if (text == null) return EventTarget.unknown;
return EventTarget.values.firstWhere(
(element) => element.name.toLowerCase() == text.toLowerCase(),
orElse: () => EventTarget.unknown,
);
}
}

View file

@ -101,6 +101,7 @@ enum SessionAccessLevel {
private, private,
contacts, contacts,
contactsPlus, contactsPlus,
registeredUsers,
anyone; anyone;
static const _readableNamesMap = { static const _readableNamesMap = {
@ -108,6 +109,7 @@ enum SessionAccessLevel {
SessionAccessLevel.private: "Private", SessionAccessLevel.private: "Private",
SessionAccessLevel.contacts: "Contacts", SessionAccessLevel.contacts: "Contacts",
SessionAccessLevel.contactsPlus: "Contacts+", SessionAccessLevel.contactsPlus: "Contacts+",
SessionAccessLevel.registeredUsers: "Registered users",
SessionAccessLevel.anyone: "Anyone", SessionAccessLevel.anyone: "Anyone",
}; };
@ -119,7 +121,7 @@ enum SessionAccessLevel {
} }
String toReadableString() { String toReadableString() {
return SessionAccessLevel._readableNamesMap[this] ?? "Unknown"; return SessionAccessLevel._readableNamesMap[this] ?? SessionAccessLevel.unknown.toReadableString();
} }
} }

View file

@ -1,58 +1,81 @@
import 'dart:convert';
import 'package:contacts_plus_plus/crypto_helper.dart'; import 'package:contacts_plus_plus/crypto_helper.dart';
import 'package:contacts_plus_plus/models/session.dart';
import 'package:contacts_plus_plus/models/session_metadata.dart'; import 'package:contacts_plus_plus/models/session_metadata.dart';
import 'package:contacts_plus_plus/models/users/online_status.dart'; import 'package:contacts_plus_plus/models/users/online_status.dart';
import 'package:crypto/crypto.dart'; import 'package:uuid/uuid.dart';
enum UserSessionType
{
unknown,
graphicalClient,
chatClient,
headless,
not;
factory UserSessionType.fromString(String? text) {
return UserSessionType.values.firstWhere((element) => element.name.toLowerCase() == text?.toLowerCase(),
orElse: () => UserSessionType.unknown,
);
}
}
class UserStatus { class UserStatus {
final OnlineStatus onlineStatus; final OnlineStatus onlineStatus;
final DateTime lastStatusChange; final DateTime lastStatusChange;
final int currentSessionAccessLevel; final DateTime lastPresenceTimestamp;
final bool currentSessionHidden; final String userSessionId;
final bool currentHosting;
final int currentSessionIndex; final int currentSessionIndex;
final List<SessionMetadata> sessions; final List<SessionMetadata> sessions;
final String appVersion; final String appVersion;
final String outputDevice; final String outputDevice;
final bool isMobile; final bool isMobile;
final bool isPresent;
final String compatibilityHash; final String compatibilityHash;
final String hashSalt; final String hashSalt;
final UserSessionType sessionType;
final List<Session> decodedSessions;
const UserStatus({ const UserStatus({
required this.onlineStatus, required this.onlineStatus,
required this.lastStatusChange, required this.lastStatusChange,
required this.lastPresenceTimestamp,
required this.userSessionId,
required this.currentSessionIndex, required this.currentSessionIndex,
required this.currentSessionAccessLevel,
required this.currentSessionHidden,
required this.currentHosting,
required this.sessions, required this.sessions,
required this.appVersion, required this.appVersion,
required this.outputDevice, required this.outputDevice,
required this.isMobile, required this.isMobile,
required this.isPresent,
required this.compatibilityHash, required this.compatibilityHash,
required this.hashSalt, required this.hashSalt,
required this.sessionType,
this.decodedSessions = const []
}); });
factory UserStatus.initial() => UserStatus.empty().copyWith( factory UserStatus.initial() =>
UserStatus.empty().copyWith(
onlineStatus: OnlineStatus.online, onlineStatus: OnlineStatus.online,
hashSalt: CryptoHelper.cryptoToken(), hashSalt: CryptoHelper.cryptoToken(),
outputDevice: "Mobile", outputDevice: "Mobile",
userSessionId: const Uuid().v4().toString(),
sessionType: UserSessionType.chatClient,
); );
factory UserStatus.empty() => UserStatus( factory UserStatus.empty() =>
UserStatus(
onlineStatus: OnlineStatus.offline, onlineStatus: OnlineStatus.offline,
lastStatusChange: DateTime.now(), lastStatusChange: DateTime.now(),
currentSessionAccessLevel: 0, lastPresenceTimestamp: DateTime.now(),
currentSessionHidden: false, userSessionId: "",
currentHosting: false,
currentSessionIndex: -1, currentSessionIndex: -1,
sessions: [], sessions: [],
appVersion: "", appVersion: "",
outputDevice: "Unknown", outputDevice: "Unknown",
isMobile: false, isMobile: false,
isPresent: false,
compatibilityHash: "", compatibilityHash: "",
hashSalt: "", hashSalt: "",
sessionType: UserSessionType.unknown
); );
factory UserStatus.fromMap(Map map) { factory UserStatus.fromMap(Map map) {
@ -60,10 +83,10 @@ class UserStatus {
final status = OnlineStatus.fromString(statusString); final status = OnlineStatus.fromString(statusString);
return UserStatus( return UserStatus(
onlineStatus: status, onlineStatus: status,
lastStatusChange: DateTime.parse(map["lastStatusChange"]), lastStatusChange: DateTime.tryParse(map["lastStatusChange"] ?? "") ?? DateTime.now(),
currentSessionAccessLevel: map["currentSessionAccessLevel"] ?? 0, lastPresenceTimestamp: DateTime.tryParse(map["lastPresenceTimestamp"] ?? "") ?? DateTime.now(),
currentSessionHidden: map["currentSessionHidden"] ?? false, userSessionId: map["userSessionId"] ?? "",
currentHosting: map["currentHosting"] ?? false, isPresent: map["isPresent"] ?? false,
currentSessionIndex: map["currentSessionIndex"] ?? -1, currentSessionIndex: map["currentSessionIndex"] ?? -1,
sessions: (map["sessions"] as List? ?? []).map((e) => SessionMetadata.fromMap(e)).toList(), sessions: (map["sessions"] as List? ?? []).map((e) => SessionMetadata.fromMap(e)).toList(),
appVersion: map["appVersion"] ?? "", appVersion: map["appVersion"] ?? "",
@ -71,6 +94,7 @@ class UserStatus {
isMobile: map["isMobile"] ?? false, isMobile: map["isMobile"] ?? false,
compatibilityHash: map["compatabilityHash"] ?? "", compatibilityHash: map["compatabilityHash"] ?? "",
hashSalt: map["hashSalt"] ?? "", hashSalt: map["hashSalt"] ?? "",
sessionType: UserSessionType.fromString(map["sessionType"])
); );
} }
@ -78,17 +102,17 @@ class UserStatus {
return { return {
"onlineStatus": onlineStatus.index, "onlineStatus": onlineStatus.index,
"lastStatusChange": lastStatusChange.toIso8601String(), "lastStatusChange": lastStatusChange.toIso8601String(),
"currentSessionAccessLevel": currentSessionAccessLevel, "isPresent": isPresent,
"currentSessionHidden": currentSessionHidden, "lastPresenceTimestamp": lastPresenceTimestamp.toIso8601String(),
"currentHosting": currentHosting, "userSessionId": userSessionId,
"currentSessionIndex": currentSessionIndex, "currentSessionIndex": currentSessionIndex,
"sessions": shallow "sessions": shallow
? [] ? []
: sessions : sessions
.map( .map(
(e) => e.toMap(), (e) => e.toMap(),
) )
.toList(), .toList(),
"appVersion": appVersion, "appVersion": appVersion,
"outputDevice": outputDevice, "outputDevice": outputDevice,
"isMobile": isMobile, "isMobile": isMobile,
@ -99,9 +123,9 @@ class UserStatus {
UserStatus copyWith({ UserStatus copyWith({
OnlineStatus? onlineStatus, OnlineStatus? onlineStatus,
DateTime? lastStatusChange, DateTime? lastStatusChange,
int? currentSessionAccessLevel, DateTime? lastPresenceTimestamp,
bool? currentSessionHidden, bool? isPresent,
bool? currentHosting, String? userSessionId,
int? currentSessionIndex, int? currentSessionIndex,
List<SessionMetadata>? sessions, List<SessionMetadata>? sessions,
String? appVersion, String? appVersion,
@ -109,13 +133,15 @@ class UserStatus {
bool? isMobile, bool? isMobile,
String? compatibilityHash, String? compatibilityHash,
String? hashSalt, String? hashSalt,
UserSessionType? sessionType,
List<Session>? sessionData,
}) => }) =>
UserStatus( UserStatus(
onlineStatus: onlineStatus ?? this.onlineStatus, onlineStatus: onlineStatus ?? this.onlineStatus,
lastStatusChange: lastStatusChange ?? this.lastStatusChange, lastStatusChange: lastStatusChange ?? this.lastStatusChange,
currentSessionAccessLevel: currentSessionAccessLevel ?? this.currentSessionAccessLevel, lastPresenceTimestamp: lastPresenceTimestamp ?? this.lastPresenceTimestamp,
currentSessionHidden: currentSessionHidden ?? this.currentSessionHidden, isPresent: isPresent ?? this.isPresent,
currentHosting: currentHosting ?? this.currentHosting, userSessionId: userSessionId ?? this.userSessionId,
currentSessionIndex: currentSessionIndex ?? this.currentSessionIndex, currentSessionIndex: currentSessionIndex ?? this.currentSessionIndex,
sessions: sessions ?? this.sessions, sessions: sessions ?? this.sessions,
appVersion: appVersion ?? this.appVersion, appVersion: appVersion ?? this.appVersion,
@ -123,5 +149,7 @@ class UserStatus {
isMobile: isMobile ?? this.isMobile, isMobile: isMobile ?? this.isMobile,
compatibilityHash: compatibilityHash ?? this.compatibilityHash, compatibilityHash: compatibilityHash ?? this.compatibilityHash,
hashSalt: hashSalt ?? this.hashSalt, hashSalt: hashSalt ?? this.hashSalt,
sessionType: sessionType ?? this.sessionType,
decodedSessions: sessionData ?? this.decodedSessions,
); );
} }

View file

@ -1,9 +1,7 @@
import 'dart:math';
import 'package:contacts_plus_plus/auxiliary.dart'; import 'package:contacts_plus_plus/auxiliary.dart';
import 'package:contacts_plus_plus/clients/messaging_client.dart'; import 'package:contacts_plus_plus/clients/messaging_client.dart';
import 'package:contacts_plus_plus/models/users/friend.dart';
import 'package:contacts_plus_plus/models/message.dart'; import 'package:contacts_plus_plus/models/message.dart';
import 'package:contacts_plus_plus/models/users/friend.dart';
import 'package:contacts_plus_plus/widgets/formatted_text.dart'; import 'package:contacts_plus_plus/widgets/formatted_text.dart';
import 'package:contacts_plus_plus/widgets/friends/friend_online_status_indicator.dart'; import 'package:contacts_plus_plus/widgets/friends/friend_online_status_indicator.dart';
import 'package:contacts_plus_plus/widgets/generic_avatar.dart'; import 'package:contacts_plus_plus/widgets/generic_avatar.dart';
@ -24,8 +22,9 @@ class FriendListTile extends StatelessWidget {
final imageUri = Aux.resdbToHttp(friend.userProfile.iconUrl); final imageUri = Aux.resdbToHttp(friend.userProfile.iconUrl);
final theme = Theme.of(context); final theme = Theme.of(context);
final mClient = Provider.of<MessagingClient>(context, listen: false); final mClient = Provider.of<MessagingClient>(context, listen: false);
final currentSessionMetadata = friend.userStatus.sessions.elementAtOrNull(max(0, friend.userStatus.currentSessionIndex)); final currentSession = friend.userStatus.currentSessionIndex == -1
final currentSession = mClient.getSessionInfo(currentSessionMetadata?.sessionHash ?? ""); ? null
: friend.userStatus.decodedSessions.elementAtOrNull(friend.userStatus.currentSessionIndex);
return ListTile( return ListTile(
leading: GenericAvatar( leading: GenericAvatar(
imageUri: imageUri, imageUri: imageUri,
@ -59,7 +58,7 @@ class FriendListTile extends StatelessWidget {
width: 4, width: 4,
), ),
Text(toBeginningOfSentenceCase(friend.userStatus.onlineStatus.name) ?? "Unknown"), Text(toBeginningOfSentenceCase(friend.userStatus.onlineStatus.name) ?? "Unknown"),
if (currentSession != null) ...[ if (currentSession != null && !currentSession.isNone) ...[
const Text(" in "), const Text(" in "),
Expanded( Expanded(
child: FormattedText( child: FormattedText(

View file

@ -1,3 +1,4 @@
import 'package:collection/collection.dart';
import 'package:contacts_plus_plus/clients/audio_cache_client.dart'; import 'package:contacts_plus_plus/clients/audio_cache_client.dart';
import 'package:contacts_plus_plus/clients/messaging_client.dart'; import 'package:contacts_plus_plus/clients/messaging_client.dart';
import 'package:contacts_plus_plus/models/users/friend.dart'; import 'package:contacts_plus_plus/models/users/friend.dart';
@ -56,7 +57,7 @@ class _MessagesListState extends State<MessagesList> with SingleTickerProviderSt
return Consumer<MessagingClient>(builder: (context, mClient, _) { return Consumer<MessagingClient>(builder: (context, mClient, _) {
final friend = mClient.selectedFriend ?? Friend.empty(); final friend = mClient.selectedFriend ?? Friend.empty();
final cache = mClient.getUserMessageCache(friend.id); final cache = mClient.getUserMessageCache(friend.id);
final sessions = friend.userStatus.sessions; final sessions = friend.userStatus.decodedSessions.whereNot((element) => element.isNone).toList();
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: Row( title: Row(
@ -121,9 +122,7 @@ class _MessagesListState extends State<MessagesList> with SingleTickerProviderSt
scrollDirection: Axis.horizontal, scrollDirection: Axis.horizontal,
itemCount: sessions.length, itemCount: sessions.length,
itemBuilder: (context, index) { itemBuilder: (context, index) {
final currentSessionMetadata = sessions[index]; final currentSession = sessions[index];
final currentSession = mClient.getSessionInfo(currentSessionMetadata.sessionHash);
if (currentSession == null) return null;
return SessionTile(session: currentSession); return SessionTile(session: currentSession);
}, },
), ),

View file

@ -181,10 +181,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: ffi name: ffi
sha256: ed5337a5660c506388a9f012be0288fb38b49020ce2b45fe1f8b8323fe429f99 sha256: "7bf0adc28a23d395f19f3f1eb21dd7cfd1dd9f8e1c50051c069122e6853bc878"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.0.2" version: "2.1.0"
file: file:
dependency: transitive dependency: transitive
description: description: