From fb30007799941a17cb63b96bb4c7b44c2b344797 Mon Sep 17 00:00:00 2001 From: Nutcake Date: Tue, 10 Oct 2023 09:53:34 +0200 Subject: [PATCH] Improve user session status tracking --- lib/clients/messaging_client.dart | 18 +++-- lib/hub_manager.dart | 2 + lib/models/session.dart | 70 ++++++++++++++----- lib/models/users/user_status.dart | 6 +- lib/widgets/friends/friend_list_tile.dart | 28 +++++--- lib/widgets/friends/friends_list_app_bar.dart | 3 +- lib/widgets/messages/messages_list.dart | 2 +- pubspec.yaml | 2 +- 8 files changed, 92 insertions(+), 39 deletions(-) diff --git a/lib/clients/messaging_client.dart b/lib/clients/messaging_client.dart index 7a7b112..19b6181 100644 --- a/lib/clients/messaging_client.dart +++ b/lib/clients/messaging_client.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'package:recon/apis/contact_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/clients/api_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/session.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:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; @@ -33,6 +35,7 @@ class MessagingClient extends ChangeNotifier { final NotificationClient _notificationClient; final HubManager _hubManager = HubManager(); final Map _sessionMap = {}; + final Set _knownSessionKeys = {}; Friend? selectedFriend; Timer? _notifyOnlineTimer; @@ -49,6 +52,8 @@ class MessagingClient extends ChangeNotifier { debugPrint("mClient created: $hashCode"); Hive.openBox(_messageBoxKey).then((box) async { await box.delete(_lastUpdateKey); + final sessions = await SessionApi.getSessions(_apiClient); + _sessionMap.addEntries(sessions.map((e) => MapEntry(e.id, e))); _setupHub(); }); } @@ -117,13 +122,13 @@ class MessagingClient extends ChangeNotifier { clearUnreadsForUser(batch.senderId); } - Future setUserStatus(UserStatus status) async { + Future setOnlineStatus(OnlineStatus status) async { final pkginfo = await PackageInfo.fromPlatform(); _userStatus = _userStatus.copyWith( appVersion: "${pkginfo.version} of ${pkginfo.appName}", lastStatusChange: DateTime.now(), - onlineStatus: status.onlineStatus, + onlineStatus: status, ); _hubManager.send( @@ -253,7 +258,7 @@ class MessagingClient extends ChangeNotifier { _hubManager.setHandler(EventTarget.removeSession, _onRemoveSession); await _hubManager.start(); - await setUserStatus(userStatus); + await setOnlineStatus(OnlineStatus.online); _hubManager.send( "InitializeStatus", responseHandler: (Map data) async { @@ -314,11 +319,16 @@ class MessagingClient extends ChangeNotifier { var status = UserStatus.fromMap(statusUpdate); final sessionMap = createSessionMap(status.hashSalt); 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); if (friend != null) { _updateContact(friend); } + for (var session in status.sessions) { + if (session.broadcastKey != null && _knownSessionKeys.add(session.broadcastKey ?? "")) { + _hubManager.send("ListenOnKey", arguments: [session.broadcastKey]); + } + } notifyListeners(); } diff --git a/lib/hub_manager.dart b/lib/hub_manager.dart index 2fd1368..7b2c0cc 100644 --- a/lib/hub_manager.dart +++ b/lib/hub_manager.dart @@ -2,6 +2,7 @@ import 'dart:convert'; import 'dart:io'; import 'package:collection/collection.dart'; +import 'package:flutter/foundation.dart'; import 'package:recon/config.dart'; import 'package:recon/models/hub_events.dart'; import 'package:logging/logging.dart'; @@ -106,6 +107,7 @@ class HubManager { void _handleInvocation(body) async { final target = EventTarget.parse(body["target"]); final args = body["arguments"] ?? []; + if (kDebugMode) _logger.info("Invocation target: ${target.name}, args:\n$args"); final handler = _handlers[target]; if (handler == null) { _logger.info("Unhandled event received"); diff --git a/lib/models/session.dart b/lib/models/session.dart index b9bb52b..1bc2791 100644 --- a/lib/models/session.dart +++ b/lib/models/session.dart @@ -1,7 +1,4 @@ -import 'dart:convert'; - import 'package:recon/string_formatter.dart'; -import 'package:crypto/crypto.dart'; class Session { final String id; @@ -39,22 +36,23 @@ class Session { factory Session.none() { return Session( - id: "", - name: "", - sessionUsers: const [], - thumbnailUrl: "", - maxUsers: 0, - hasEnded: true, - isValid: false, - description: "", - tags: const [], - headlessHost: false, - hostUserId: "", - hostUsername: "", - accessLevel: SessionAccessLevel.unknown); + id: "", + name: "", + sessionUsers: const [], + thumbnailUrl: "", + maxUsers: 0, + hasEnded: true, + isValid: false, + description: "", + tags: const [], + headlessHost: false, + hostUserId: "", + hostUsername: "", + accessLevel: SessionAccessLevel.unknown, + ); } - bool get isNone => id.isEmpty && isValid == false; + bool get isVisible => name.isNotEmpty && accessLevel != SessionAccessLevel.unknown; factory Session.fromMap(Map? map) { if (map == null) return Session.none(); @@ -93,6 +91,40 @@ class Session { }; } + Session copyWith({ + String? id, + String? name, + FormatNode? formattedName, + List? sessionUsers, + String? thumbnailUrl, + int? maxUsers, + bool? hasEnded, + bool? isValid, + String? description, + FormatNode? formattedDescription, + List? 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; } @@ -107,10 +139,10 @@ enum SessionAccessLevel { static const _readableNamesMap = { SessionAccessLevel.unknown: "Unknown", SessionAccessLevel.private: "Private", - SessionAccessLevel.contacts: "Contacts", + SessionAccessLevel.contacts: "Contacts only", SessionAccessLevel.contactsPlus: "Contacts+", SessionAccessLevel.registeredUsers: "Registered users", - SessionAccessLevel.anyone: "Anyone", + SessionAccessLevel.anyone: "Public", }; factory SessionAccessLevel.fromName(String? name) { diff --git a/lib/models/users/user_status.dart b/lib/models/users/user_status.dart index 1299653..94ac818 100644 --- a/lib/models/users/user_status.dart +++ b/lib/models/users/user_status.dart @@ -56,7 +56,7 @@ class UserStatus { UserStatus.empty().copyWith( onlineStatus: OnlineStatus.online, hashSalt: CryptoHelper.cryptoToken(), - outputDevice: "Mobile", + outputDevice: "Screen", userSessionId: const Uuid().v4().toString(), sessionType: UserSessionType.chatClient, ); @@ -134,7 +134,7 @@ class UserStatus { String? compatibilityHash, String? hashSalt, UserSessionType? sessionType, - List? sessionData, + List? decodedSessions, }) => UserStatus( onlineStatus: onlineStatus ?? this.onlineStatus, @@ -150,6 +150,6 @@ class UserStatus { compatibilityHash: compatibilityHash ?? this.compatibilityHash, hashSalt: hashSalt ?? this.hashSalt, sessionType: sessionType ?? this.sessionType, - decodedSessions: sessionData ?? this.decodedSessions, + decodedSessions: decodedSessions ?? this.decodedSessions, ); } diff --git a/lib/widgets/friends/friend_list_tile.dart b/lib/widgets/friends/friend_list_tile.dart index 5943782..d10ef14 100644 --- a/lib/widgets/friends/friend_list_tile.dart +++ b/lib/widgets/friends/friend_list_tile.dart @@ -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/clients/messaging_client.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/generic_avatar.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 { const FriendListTile({required this.friend, required this.unreads, this.onTap, super.key}); @@ -58,14 +58,24 @@ class FriendListTile extends StatelessWidget { width: 4, ), Text(toBeginningOfSentenceCase(friend.userStatus.onlineStatus.name) ?? "Unknown"), - if (currentSession != null && !currentSession.isNone) ...[ + if (currentSession != null && currentSession.isVisible) ...[ const Text(" in "), - Expanded( + if (currentSession.name.isNotEmpty) + Expanded( child: FormattedText( - currentSession.formattedName, - overflow: TextOverflow.ellipsis, - maxLines: 1, - )) + currentSession.formattedName, + overflow: TextOverflow.ellipsis, + maxLines: 1, + ), + ) + else + Expanded( + child: Text( + "${currentSession.accessLevel.toReadableString()} session", + overflow: TextOverflow.ellipsis, + maxLines: 1, + ), + ) ] ], ), diff --git a/lib/widgets/friends/friends_list_app_bar.dart b/lib/widgets/friends/friends_list_app_bar.dart index e372813..45bf404 100644 --- a/lib/widgets/friends/friends_list_app_bar.dart +++ b/lib/widgets/friends/friends_list_app_bar.dart @@ -42,10 +42,9 @@ class _FriendsListAppBarState extends State with AutomaticKee ], ), onSelected: (OnlineStatus onlineStatus) async { - final newStatus = client.userStatus.copyWith(onlineStatus: onlineStatus); final settingsClient = ClientHolder.of(context).settingsClient; try { - await client.setUserStatus(newStatus); + await client.setOnlineStatus(onlineStatus); await settingsClient .changeSettings(settingsClient.currentSettings.copyWith(lastOnlineStatus: onlineStatus.index)); } catch (e, s) { diff --git a/lib/widgets/messages/messages_list.dart b/lib/widgets/messages/messages_list.dart index 416364a..ef3590e 100644 --- a/lib/widgets/messages/messages_list.dart +++ b/lib/widgets/messages/messages_list.dart @@ -57,7 +57,7 @@ class _MessagesListState extends State with SingleTickerProviderSt return Consumer(builder: (context, mClient, _) { final friend = mClient.selectedFriend ?? Friend.empty(); 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( appBar: AppBar( title: Row( diff --git a/pubspec.yaml b/pubspec.yaml index 0097803..8c362c5 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -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 # 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. -version: 1.0.0+1 +version: 0.9.0+1 environment: sdk: '>=3.0.1'