Improve user session status tracking

This commit is contained in:
Nutcake 2023-10-10 09:53:34 +02:00
parent 8c3703cde9
commit fb30007799
8 changed files with 92 additions and 39 deletions

View file

@ -2,6 +2,7 @@ import 'dart:async';
import 'package:recon/apis/contact_api.dart'; import 'package:recon/apis/contact_api.dart';
import 'package:recon/apis/message_api.dart'; import 'package:recon/apis/message_api.dart';
import 'package:recon/apis/session_api.dart';
import 'package:recon/apis/user_api.dart'; import 'package:recon/apis/user_api.dart';
import 'package:recon/clients/api_client.dart'; import 'package:recon/clients/api_client.dart';
import 'package:recon/clients/notification_client.dart'; import 'package:recon/clients/notification_client.dart';
@ -11,6 +12,7 @@ import 'package:recon/models/hub_events.dart';
import 'package:recon/models/message.dart'; import 'package:recon/models/message.dart';
import 'package:recon/models/session.dart'; import 'package:recon/models/session.dart';
import 'package:recon/models/users/friend.dart'; import 'package:recon/models/users/friend.dart';
import 'package:recon/models/users/online_status.dart';
import 'package:recon/models/users/user_status.dart'; import 'package:recon/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';
@ -33,6 +35,7 @@ class MessagingClient extends ChangeNotifier {
final NotificationClient _notificationClient; final NotificationClient _notificationClient;
final HubManager _hubManager = HubManager(); final HubManager _hubManager = HubManager();
final Map<String, Session> _sessionMap = {}; final Map<String, Session> _sessionMap = {};
final Set<String> _knownSessionKeys = {};
Friend? selectedFriend; Friend? selectedFriend;
Timer? _notifyOnlineTimer; Timer? _notifyOnlineTimer;
@ -49,6 +52,8 @@ 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 sessions = await SessionApi.getSessions(_apiClient);
_sessionMap.addEntries(sessions.map((e) => MapEntry(e.id, e)));
_setupHub(); _setupHub();
}); });
} }
@ -117,13 +122,13 @@ class MessagingClient extends ChangeNotifier {
clearUnreadsForUser(batch.senderId); clearUnreadsForUser(batch.senderId);
} }
Future<void> setUserStatus(UserStatus status) async { Future<void> setOnlineStatus(OnlineStatus status) async {
final pkginfo = await PackageInfo.fromPlatform(); final pkginfo = await PackageInfo.fromPlatform();
_userStatus = _userStatus.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, onlineStatus: status,
); );
_hubManager.send( _hubManager.send(
@ -253,7 +258,7 @@ class MessagingClient extends ChangeNotifier {
_hubManager.setHandler(EventTarget.removeSession, _onRemoveSession); _hubManager.setHandler(EventTarget.removeSession, _onRemoveSession);
await _hubManager.start(); await _hubManager.start();
await setUserStatus(userStatus); await setOnlineStatus(OnlineStatus.online);
_hubManager.send( _hubManager.send(
"InitializeStatus", "InitializeStatus",
responseHandler: (Map data) async { responseHandler: (Map data) async {
@ -314,11 +319,16 @@ class MessagingClient extends ChangeNotifier {
var status = UserStatus.fromMap(statusUpdate); var status = UserStatus.fromMap(statusUpdate);
final sessionMap = createSessionMap(status.hashSalt); final sessionMap = createSessionMap(status.hashSalt);
status = status.copyWith( status = status.copyWith(
sessionData: status.sessions.map((e) => sessionMap[e.sessionHash] ?? Session.none()).toList()); decodedSessions: status.sessions.map((e) => sessionMap[e.sessionHash] ?? Session.none().copyWith(accessLevel: e.accessLevel)).toList());
final friend = getAsFriend(statusUpdate["userId"])?.copyWith(userStatus: status); final friend = getAsFriend(statusUpdate["userId"])?.copyWith(userStatus: status);
if (friend != null) { if (friend != null) {
_updateContact(friend); _updateContact(friend);
} }
for (var session in status.sessions) {
if (session.broadcastKey != null && _knownSessionKeys.add(session.broadcastKey ?? "")) {
_hubManager.send("ListenOnKey", arguments: [session.broadcastKey]);
}
}
notifyListeners(); notifyListeners();
} }

View file

@ -2,6 +2,7 @@ import 'dart:convert';
import 'dart:io'; import 'dart:io';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:flutter/foundation.dart';
import 'package:recon/config.dart'; import 'package:recon/config.dart';
import 'package:recon/models/hub_events.dart'; import 'package:recon/models/hub_events.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
@ -106,6 +107,7 @@ class HubManager {
void _handleInvocation(body) async { void _handleInvocation(body) async {
final target = EventTarget.parse(body["target"]); final target = EventTarget.parse(body["target"]);
final args = body["arguments"] ?? []; final args = body["arguments"] ?? [];
if (kDebugMode) _logger.info("Invocation target: ${target.name}, args:\n$args");
final handler = _handlers[target]; final handler = _handlers[target];
if (handler == null) { if (handler == null) {
_logger.info("Unhandled event received"); _logger.info("Unhandled event received");

View file

@ -1,7 +1,4 @@
import 'dart:convert';
import 'package:recon/string_formatter.dart'; import 'package:recon/string_formatter.dart';
import 'package:crypto/crypto.dart';
class Session { class Session {
final String id; final String id;
@ -39,22 +36,23 @@ class Session {
factory Session.none() { factory Session.none() {
return Session( return Session(
id: "", id: "",
name: "", name: "",
sessionUsers: const [], sessionUsers: const [],
thumbnailUrl: "", thumbnailUrl: "",
maxUsers: 0, maxUsers: 0,
hasEnded: true, hasEnded: true,
isValid: false, isValid: false,
description: "", description: "",
tags: const [], tags: const [],
headlessHost: false, headlessHost: false,
hostUserId: "", hostUserId: "",
hostUsername: "", hostUsername: "",
accessLevel: SessionAccessLevel.unknown); accessLevel: SessionAccessLevel.unknown,
);
} }
bool get isNone => id.isEmpty && isValid == false; bool get isVisible => name.isNotEmpty && accessLevel != SessionAccessLevel.unknown;
factory Session.fromMap(Map? map) { factory Session.fromMap(Map? map) {
if (map == null) return Session.none(); if (map == null) return Session.none();
@ -93,6 +91,40 @@ class Session {
}; };
} }
Session copyWith({
String? id,
String? name,
FormatNode? formattedName,
List<SessionUser>? sessionUsers,
String? thumbnailUrl,
int? maxUsers,
bool? hasEnded,
bool? isValid,
String? description,
FormatNode? formattedDescription,
List<String>? tags,
bool? headlessHost,
String? hostUserId,
String? hostUsername,
SessionAccessLevel? accessLevel,
}) {
return Session(
id: id ?? this.id,
name: name ?? this.name,
sessionUsers: sessionUsers ?? this.sessionUsers,
thumbnailUrl: thumbnailUrl ?? this.thumbnailUrl,
maxUsers: maxUsers ?? this.maxUsers,
hasEnded: hasEnded ?? this.hasEnded,
isValid: isValid ?? this.isValid,
description: description ?? this.description,
tags: tags ?? this.tags,
headlessHost: headlessHost ?? this.headlessHost,
hostUserId: hostUserId ?? this.hostUserId,
hostUsername: hostUsername ?? this.hostUsername,
accessLevel: accessLevel ?? this.accessLevel,
);
}
bool get isLive => !hasEnded && isValid; bool get isLive => !hasEnded && isValid;
} }
@ -107,10 +139,10 @@ enum SessionAccessLevel {
static const _readableNamesMap = { static const _readableNamesMap = {
SessionAccessLevel.unknown: "Unknown", SessionAccessLevel.unknown: "Unknown",
SessionAccessLevel.private: "Private", SessionAccessLevel.private: "Private",
SessionAccessLevel.contacts: "Contacts", SessionAccessLevel.contacts: "Contacts only",
SessionAccessLevel.contactsPlus: "Contacts+", SessionAccessLevel.contactsPlus: "Contacts+",
SessionAccessLevel.registeredUsers: "Registered users", SessionAccessLevel.registeredUsers: "Registered users",
SessionAccessLevel.anyone: "Anyone", SessionAccessLevel.anyone: "Public",
}; };
factory SessionAccessLevel.fromName(String? name) { factory SessionAccessLevel.fromName(String? name) {

View file

@ -56,7 +56,7 @@ class UserStatus {
UserStatus.empty().copyWith( UserStatus.empty().copyWith(
onlineStatus: OnlineStatus.online, onlineStatus: OnlineStatus.online,
hashSalt: CryptoHelper.cryptoToken(), hashSalt: CryptoHelper.cryptoToken(),
outputDevice: "Mobile", outputDevice: "Screen",
userSessionId: const Uuid().v4().toString(), userSessionId: const Uuid().v4().toString(),
sessionType: UserSessionType.chatClient, sessionType: UserSessionType.chatClient,
); );
@ -134,7 +134,7 @@ class UserStatus {
String? compatibilityHash, String? compatibilityHash,
String? hashSalt, String? hashSalt,
UserSessionType? sessionType, UserSessionType? sessionType,
List<Session>? sessionData, List<Session>? decodedSessions,
}) => }) =>
UserStatus( UserStatus(
onlineStatus: onlineStatus ?? this.onlineStatus, onlineStatus: onlineStatus ?? this.onlineStatus,
@ -150,6 +150,6 @@ class UserStatus {
compatibilityHash: compatibilityHash ?? this.compatibilityHash, compatibilityHash: compatibilityHash ?? this.compatibilityHash,
hashSalt: hashSalt ?? this.hashSalt, hashSalt: hashSalt ?? this.hashSalt,
sessionType: sessionType ?? this.sessionType, sessionType: sessionType ?? this.sessionType,
decodedSessions: sessionData ?? this.decodedSessions, decodedSessions: decodedSessions ?? this.decodedSessions,
); );
} }

View file

@ -1,3 +1,6 @@
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:provider/provider.dart';
import 'package:recon/auxiliary.dart'; import 'package:recon/auxiliary.dart';
import 'package:recon/clients/messaging_client.dart'; import 'package:recon/clients/messaging_client.dart';
import 'package:recon/models/message.dart'; import 'package:recon/models/message.dart';
@ -6,9 +9,6 @@ import 'package:recon/widgets/formatted_text.dart';
import 'package:recon/widgets/friends/friend_online_status_indicator.dart'; import 'package:recon/widgets/friends/friend_online_status_indicator.dart';
import 'package:recon/widgets/generic_avatar.dart'; import 'package:recon/widgets/generic_avatar.dart';
import 'package:recon/widgets/messages/messages_list.dart'; import 'package:recon/widgets/messages/messages_list.dart';
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:provider/provider.dart';
class FriendListTile extends StatelessWidget { class FriendListTile extends StatelessWidget {
const FriendListTile({required this.friend, required this.unreads, this.onTap, super.key}); const FriendListTile({required this.friend, required this.unreads, this.onTap, super.key});
@ -58,14 +58,24 @@ 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 && !currentSession.isNone) ...[ if (currentSession != null && currentSession.isVisible) ...[
const Text(" in "), const Text(" in "),
Expanded( if (currentSession.name.isNotEmpty)
Expanded(
child: FormattedText( child: FormattedText(
currentSession.formattedName, currentSession.formattedName,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
maxLines: 1, maxLines: 1,
)) ),
)
else
Expanded(
child: Text(
"${currentSession.accessLevel.toReadableString()} session",
overflow: TextOverflow.ellipsis,
maxLines: 1,
),
)
] ]
], ],
), ),

View file

@ -42,10 +42,9 @@ class _FriendsListAppBarState extends State<FriendsListAppBar> with AutomaticKee
], ],
), ),
onSelected: (OnlineStatus onlineStatus) async { onSelected: (OnlineStatus onlineStatus) async {
final newStatus = client.userStatus.copyWith(onlineStatus: onlineStatus);
final settingsClient = ClientHolder.of(context).settingsClient; final settingsClient = ClientHolder.of(context).settingsClient;
try { try {
await client.setUserStatus(newStatus); await client.setOnlineStatus(onlineStatus);
await settingsClient await settingsClient
.changeSettings(settingsClient.currentSettings.copyWith(lastOnlineStatus: onlineStatus.index)); .changeSettings(settingsClient.currentSettings.copyWith(lastOnlineStatus: onlineStatus.index));
} catch (e, s) { } catch (e, s) {

View file

@ -57,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.decodedSessions.whereNot((element) => element.isNone).toList(); final sessions = friend.userStatus.decodedSessions.where((element) => element.isVisible).toList();
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: Row( title: Row(

View file

@ -16,7 +16,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
# In Windows, build-name is used as the major, minor, and patch parts # In Windows, build-name is used as the major, minor, and patch parts
# of the product and file versions while build-number is used as the build suffix. # of the product and file versions while build-number is used as the build suffix.
version: 1.0.0+1 version: 0.9.0+1
environment: environment:
sdk: '>=3.0.1' sdk: '>=3.0.1'