Improve user session status tracking
This commit is contained in:
commit
fd5b234ba9
8 changed files with 92 additions and 39 deletions
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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");
|
||||||
|
|
|
@ -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;
|
||||||
|
@ -51,10 +48,11 @@ class Session {
|
||||||
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) {
|
||||||
|
|
|
@ -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,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 "),
|
||||||
|
if (currentSession.name.isNotEmpty)
|
||||||
Expanded(
|
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,
|
||||||
|
),
|
||||||
|
)
|
||||||
]
|
]
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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'
|
||||||
|
|
Loading…
Reference in a new issue