Improve user session status tracking

This commit is contained in:
Nutcake 2023-10-10 09:54:42 +02:00 committed by GitHub
commit fd5b234ba9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
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/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<String, Session> _sessionMap = {};
final Set<String> _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<void> setUserStatus(UserStatus status) async {
Future<void> 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();
}

View file

@ -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");

View file

@ -1,7 +1,4 @@
import 'dart:convert';
import 'package:recon/string_formatter.dart';
import 'package:crypto/crypto.dart';
class Session {
final String id;
@ -51,10 +48,11 @@ class Session {
headlessHost: false,
hostUserId: "",
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) {
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;
}
@ -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) {

View file

@ -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<Session>? sessionData,
List<Session>? 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,
);
}

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/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 "),
if (currentSession.name.isNotEmpty)
Expanded(
child: FormattedText(
currentSession.formattedName,
overflow: TextOverflow.ellipsis,
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 {
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) {

View file

@ -57,7 +57,7 @@ class _MessagesListState extends State<MessagesList> with SingleTickerProviderSt
return Consumer<MessagingClient>(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(

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
# 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'