Add preliminary hash based session handling
This commit is contained in:
parent
c1da0da897
commit
56ed403d79
8 changed files with 148 additions and 170 deletions
|
@ -1,6 +1,8 @@
|
|||
import 'dart:async';
|
||||
import 'package:contacts_plus_plus/apis/session_api.dart';
|
||||
import 'package:contacts_plus_plus/crypto_helper.dart';
|
||||
import 'package:contacts_plus_plus/hub_manager.dart';
|
||||
import 'package:contacts_plus_plus/models/users/online_status.dart';
|
||||
import 'package:contacts_plus_plus/models/session.dart';
|
||||
import 'package:contacts_plus_plus/models/users/user_status.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
@ -58,13 +60,14 @@ class MessagingClient extends ChangeNotifier {
|
|||
final Logger _logger = Logger("Messaging");
|
||||
final NotificationClient _notificationClient;
|
||||
final HubManager _hubManager = HubManager();
|
||||
final Map<String, Session> _sessionMap = {};
|
||||
Friend? selectedFriend;
|
||||
|
||||
Timer? _notifyOnlineTimer;
|
||||
Timer? _autoRefresh;
|
||||
Timer? _unreadSafeguard;
|
||||
String? _initStatus;
|
||||
UserStatus _userStatus = UserStatus.empty().copyWith(onlineStatus: OnlineStatus.online);
|
||||
UserStatus _userStatus = UserStatus.initial();
|
||||
|
||||
UserStatus get userStatus => _userStatus;
|
||||
|
||||
|
@ -73,9 +76,14 @@ class MessagingClient extends ChangeNotifier {
|
|||
_notificationClient = notificationClient {
|
||||
debugPrint("mClient created: $hashCode");
|
||||
Hive.openBox(_messageBoxKey).then((box) async {
|
||||
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();
|
||||
}
|
||||
|
||||
@override
|
||||
|
@ -105,6 +113,8 @@ class MessagingClient extends ChangeNotifier {
|
|||
|
||||
MessageCache _createUserMessageCache(String userId) => MessageCache(apiClient: _apiClient, userId: userId);
|
||||
|
||||
Session? getSessionInfo(String idHash) => _sessionMap[idHash];
|
||||
|
||||
Future<void> refreshFriendsListWithErrorHandler() async {
|
||||
try {
|
||||
await refreshFriendsList();
|
||||
|
@ -147,17 +157,19 @@ class MessagingClient extends ChangeNotifier {
|
|||
|
||||
_userStatus = status.copyWith(
|
||||
appVersion: "${pkginfo.version} of ${pkginfo.appName}",
|
||||
isMobile: true,
|
||||
lastStatusChange: DateTime.now(),
|
||||
);
|
||||
|
||||
_hubManager.send("BroadcastStatus", arguments: [
|
||||
_userStatus.toMap(),
|
||||
{
|
||||
"group": 0,
|
||||
"targetIds": [],
|
||||
}
|
||||
]);
|
||||
_hubManager.send(
|
||||
"BroadcastStatus",
|
||||
arguments: [
|
||||
_userStatus.toMap(),
|
||||
{
|
||||
"group": 0,
|
||||
"targetIds": [],
|
||||
}
|
||||
],
|
||||
);
|
||||
|
||||
final self = getAsFriend(_apiClient.userId);
|
||||
await _updateContact(self!.copyWith(userStatus: _userStatus));
|
||||
|
@ -272,6 +284,7 @@ class MessagingClient extends ChangeNotifier {
|
|||
_hubManager.setHandler(EventTarget.receiveSessionUpdate, _onReceiveSessionUpdate);
|
||||
|
||||
await _hubManager.start();
|
||||
setUserStatus(userStatus);
|
||||
_hubManager.send(
|
||||
"InitializeStatus",
|
||||
responseHandler: (Map data) async {
|
||||
|
@ -335,5 +348,12 @@ class MessagingClient extends ChangeNotifier {
|
|||
notifyListeners();
|
||||
}
|
||||
|
||||
void _onReceiveSessionUpdate(List args) {}
|
||||
void _onReceiveSessionUpdate(List args) {
|
||||
for (final sessionUpdate in args) {
|
||||
final session = Session.fromMap(sessionUpdate);
|
||||
final idHash = CryptoHelper.idHash(session.id + _userStatus.hashSalt);
|
||||
_sessionMap[idHash] = session;
|
||||
}
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
|
14
lib/crypto_helper.dart
Normal file
14
lib/crypto_helper.dart
Normal file
|
@ -0,0 +1,14 @@
|
|||
import 'dart:convert';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:crypto/crypto.dart';
|
||||
|
||||
class CryptoHelper {
|
||||
static final Random _random = Random.secure();
|
||||
|
||||
static List<int> randomBytes(int length) => List<int>.generate(length, (i) => _random.nextInt(256));
|
||||
|
||||
static String cryptoToken([int length = 128]) => base64UrlEncode(randomBytes(length)).replaceAll("/", "_");
|
||||
|
||||
static String idHash(String id) => sha256.convert(utf8.encode(id)).toString().replaceAll("-", "").toUpperCase();
|
||||
}
|
|
@ -1,4 +1,7 @@
|
|||
import 'dart:convert';
|
||||
|
||||
import 'package:contacts_plus_plus/string_formatter.dart';
|
||||
import 'package:crypto/crypto.dart';
|
||||
|
||||
class Session {
|
||||
final String id;
|
||||
|
@ -96,15 +99,15 @@ class Session {
|
|||
enum SessionAccessLevel {
|
||||
unknown,
|
||||
private,
|
||||
friends,
|
||||
friendsOfFriends,
|
||||
contacts,
|
||||
contactsPlus,
|
||||
anyone;
|
||||
|
||||
static const _readableNamesMap = {
|
||||
SessionAccessLevel.unknown: "Unknown",
|
||||
SessionAccessLevel.private: "Private",
|
||||
SessionAccessLevel.friends: "Contacts",
|
||||
SessionAccessLevel.friendsOfFriends: "Contacts+",
|
||||
SessionAccessLevel.contacts: "Contacts",
|
||||
SessionAccessLevel.contactsPlus: "Contacts+",
|
||||
SessionAccessLevel.anyone: "Anyone",
|
||||
};
|
||||
|
||||
|
|
38
lib/models/session_metadata.dart
Normal file
38
lib/models/session_metadata.dart
Normal file
|
@ -0,0 +1,38 @@
|
|||
import 'package:contacts_plus_plus/models/session.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
|
||||
class SessionMetadata {
|
||||
final String sessionHash;
|
||||
final SessionAccessLevel accessLevel;
|
||||
final bool sessionHidden;
|
||||
final bool? isHost;
|
||||
final String? broadcastKey;
|
||||
|
||||
SessionMetadata({
|
||||
required this.sessionHash,
|
||||
required this.accessLevel,
|
||||
required this.sessionHidden,
|
||||
required this.isHost,
|
||||
required this.broadcastKey,
|
||||
});
|
||||
|
||||
factory SessionMetadata.fromMap(Map map) {
|
||||
return SessionMetadata(
|
||||
sessionHash: map["sessionHash"],
|
||||
accessLevel: SessionAccessLevel.fromName(map["accessLevel"]),
|
||||
sessionHidden: map["sessionHidden"],
|
||||
isHost: map["ishost"],
|
||||
broadcastKey: map["broadcastKey"],
|
||||
);
|
||||
}
|
||||
|
||||
Map toMap() {
|
||||
return {
|
||||
"sessionHash": sessionHash,
|
||||
"accessLevel": toBeginningOfSentenceCase(accessLevel.name),
|
||||
"sessionHidden": sessionHidden,
|
||||
"isHost": isHost,
|
||||
"broadcastKey": broadcastKey,
|
||||
};
|
||||
}
|
||||
}
|
|
@ -1,5 +1,9 @@
|
|||
import 'package:contacts_plus_plus/models/session.dart';
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:contacts_plus_plus/crypto_helper.dart';
|
||||
import 'package:contacts_plus_plus/models/session_metadata.dart';
|
||||
import 'package:contacts_plus_plus/models/users/online_status.dart';
|
||||
import 'package:crypto/crypto.dart';
|
||||
|
||||
class UserStatus {
|
||||
final OnlineStatus onlineStatus;
|
||||
|
@ -7,56 +11,67 @@ class UserStatus {
|
|||
final int currentSessionAccessLevel;
|
||||
final bool currentSessionHidden;
|
||||
final bool currentHosting;
|
||||
final Session currentSession;
|
||||
final List<Session> activeSessions;
|
||||
final int currentSessionIndex;
|
||||
final List<SessionMetadata> sessions;
|
||||
final String appVersion;
|
||||
final String outputDevice;
|
||||
final bool isMobile;
|
||||
final String compatibilityHash;
|
||||
final String hashSalt;
|
||||
|
||||
const UserStatus({
|
||||
required this.onlineStatus,
|
||||
required this.lastStatusChange,
|
||||
required this.currentSession,
|
||||
required this.currentSessionIndex,
|
||||
required this.currentSessionAccessLevel,
|
||||
required this.currentSessionHidden,
|
||||
required this.currentHosting,
|
||||
required this.activeSessions,
|
||||
required this.sessions,
|
||||
required this.appVersion,
|
||||
required this.outputDevice,
|
||||
required this.isMobile,
|
||||
required this.compatibilityHash,
|
||||
required this.hashSalt,
|
||||
});
|
||||
|
||||
factory UserStatus.initial() => UserStatus.empty().copyWith(
|
||||
onlineStatus: OnlineStatus.online,
|
||||
hashSalt: CryptoHelper.cryptoToken(),
|
||||
outputDevice: "Mobile",
|
||||
);
|
||||
|
||||
factory UserStatus.empty() => UserStatus(
|
||||
onlineStatus: OnlineStatus.offline,
|
||||
lastStatusChange: DateTime.now(),
|
||||
currentSessionAccessLevel: 0,
|
||||
currentSessionHidden: false,
|
||||
currentHosting: false,
|
||||
currentSession: Session.none(),
|
||||
activeSessions: [],
|
||||
currentSessionIndex: -1,
|
||||
sessions: [],
|
||||
appVersion: "",
|
||||
outputDevice: "Unknown",
|
||||
isMobile: false,
|
||||
compatibilityHash: "",
|
||||
hashSalt: "",
|
||||
);
|
||||
|
||||
factory UserStatus.fromMap(Map map) {
|
||||
final statusString = map["onlineStatus"].toString();
|
||||
final status = OnlineStatus.fromString(statusString);
|
||||
return UserStatus(
|
||||
onlineStatus: status,
|
||||
lastStatusChange: DateTime.parse(map["lastStatusChange"]),
|
||||
currentSessionAccessLevel: map["currentSessionAccessLevel"] ?? 0,
|
||||
currentSessionHidden: map["currentSessionHidden"] ?? false,
|
||||
currentHosting: map["currentHosting"] ?? false,
|
||||
currentSession: Session.fromMap(map["currentSession"]),
|
||||
activeSessions: (map["activeSessions"] as List? ?? []).map((e) => Session.fromMap(e)).toList(),
|
||||
appVersion: map["appVersion"] ?? "",
|
||||
outputDevice: map["outputDevice"] ?? "Unknown",
|
||||
isMobile: map["isMobile"] ?? false,
|
||||
compatibilityHash: map["compatabilityHash"] ?? "");
|
||||
onlineStatus: status,
|
||||
lastStatusChange: DateTime.parse(map["lastStatusChange"]),
|
||||
currentSessionAccessLevel: map["currentSessionAccessLevel"] ?? 0,
|
||||
currentSessionHidden: map["currentSessionHidden"] ?? false,
|
||||
currentHosting: map["currentHosting"] ?? false,
|
||||
currentSessionIndex: map["currentSessionIndex"] ?? -1,
|
||||
sessions: (map["sessions"] as List? ?? []).map((e) => SessionMetadata.fromMap(e)).toList(),
|
||||
appVersion: map["appVersion"] ?? "",
|
||||
outputDevice: map["outputDevice"] ?? "Unknown",
|
||||
isMobile: map["isMobile"] ?? false,
|
||||
compatibilityHash: map["compatabilityHash"] ?? "",
|
||||
hashSalt: map["hashSalt"] ?? "",
|
||||
);
|
||||
}
|
||||
|
||||
Map toMap({bool shallow = false}) {
|
||||
|
@ -66,10 +81,10 @@ class UserStatus {
|
|||
"currentSessionAccessLevel": currentSessionAccessLevel,
|
||||
"currentSessionHidden": currentSessionHidden,
|
||||
"currentHosting": currentHosting,
|
||||
"currentSession": currentSession.isNone || shallow ? null : currentSession.toMap(),
|
||||
"activeSessions": shallow
|
||||
"currentSessionIndex": currentSessionIndex,
|
||||
"sessions": shallow
|
||||
? []
|
||||
: activeSessions
|
||||
: sessions
|
||||
.map(
|
||||
(e) => e.toMap(),
|
||||
)
|
||||
|
@ -87,12 +102,13 @@ class UserStatus {
|
|||
int? currentSessionAccessLevel,
|
||||
bool? currentSessionHidden,
|
||||
bool? currentHosting,
|
||||
Session? currentSession,
|
||||
List<Session>? activeSessions,
|
||||
int? currentSessionIndex,
|
||||
List<SessionMetadata>? sessions,
|
||||
String? appVersion,
|
||||
String? outputDevice,
|
||||
bool? isMobile,
|
||||
String? compatibilityHash,
|
||||
String? hashSalt,
|
||||
}) =>
|
||||
UserStatus(
|
||||
onlineStatus: onlineStatus ?? this.onlineStatus,
|
||||
|
@ -100,11 +116,12 @@ class UserStatus {
|
|||
currentSessionAccessLevel: currentSessionAccessLevel ?? this.currentSessionAccessLevel,
|
||||
currentSessionHidden: currentSessionHidden ?? this.currentSessionHidden,
|
||||
currentHosting: currentHosting ?? this.currentHosting,
|
||||
currentSession: currentSession ?? this.currentSession,
|
||||
activeSessions: activeSessions ?? this.activeSessions,
|
||||
currentSessionIndex: currentSessionIndex ?? this.currentSessionIndex,
|
||||
sessions: sessions ?? this.sessions,
|
||||
appVersion: appVersion ?? this.appVersion,
|
||||
outputDevice: outputDevice ?? this.outputDevice,
|
||||
isMobile: isMobile ?? this.isMobile,
|
||||
compatibilityHash: compatibilityHash ?? this.compatibilityHash,
|
||||
hashSalt: hashSalt ?? this.hashSalt,
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import 'dart:math';
|
||||
|
||||
import 'package:contacts_plus_plus/auxiliary.dart';
|
||||
import 'package:contacts_plus_plus/clients/messaging_client.dart';
|
||||
import 'package:contacts_plus_plus/models/users/friend.dart';
|
||||
|
@ -21,6 +23,9 @@ class FriendListTile extends StatelessWidget {
|
|||
Widget build(BuildContext context) {
|
||||
final imageUri = Aux.resdbToHttp(friend.userProfile.iconUrl);
|
||||
final theme = Theme.of(context);
|
||||
final mClient = Provider.of<MessagingClient>(context, listen: false);
|
||||
final currentSessionMetadata = friend.userStatus.sessions.elementAtOrNull(max(0, friend.userStatus.currentSessionIndex));
|
||||
final currentSession = mClient.getSessionInfo(currentSessionMetadata?.sessionHash ?? "");
|
||||
return ListTile(
|
||||
leading: GenericAvatar(
|
||||
imageUri: imageUri,
|
||||
|
@ -54,11 +59,11 @@ class FriendListTile extends StatelessWidget {
|
|||
width: 4,
|
||||
),
|
||||
Text(toBeginningOfSentenceCase(friend.userStatus.onlineStatus.name) ?? "Unknown"),
|
||||
if (!friend.userStatus.currentSession.isNone) ...[
|
||||
if (currentSession != null) ...[
|
||||
const Text(" in "),
|
||||
Expanded(
|
||||
child: FormattedText(
|
||||
friend.userStatus.currentSession.formattedName,
|
||||
currentSession.formattedName,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
maxLines: 1,
|
||||
))
|
||||
|
@ -67,7 +72,6 @@ class FriendListTile extends StatelessWidget {
|
|||
),
|
||||
onTap: () async {
|
||||
onTap?.call();
|
||||
final mClient = Provider.of<MessagingClient>(context, listen: false);
|
||||
mClient.loadUserMessageCache(friend.id);
|
||||
final unreads = mClient.getUnreadsForFriend(friend);
|
||||
if (unreads.isNotEmpty) {
|
||||
|
|
|
@ -6,7 +6,6 @@ import 'package:contacts_plus_plus/widgets/friends/friend_online_status_indicato
|
|||
import 'package:contacts_plus_plus/widgets/messages/message_input_bar.dart';
|
||||
import 'package:contacts_plus_plus/widgets/messages/messages_session_header.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import 'message_bubble.dart';
|
||||
|
@ -57,7 +56,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.activeSessions;
|
||||
final sessions = friend.userStatus.sessions;
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Row(
|
||||
|
@ -121,7 +120,12 @@ class _MessagesListState extends State<MessagesList> with SingleTickerProviderSt
|
|||
controller: _sessionListScrollController,
|
||||
scrollDirection: Axis.horizontal,
|
||||
itemCount: sessions.length,
|
||||
itemBuilder: (context, index) => SessionTile(session: sessions[index]),
|
||||
itemBuilder: (context, index) {
|
||||
final currentSessionMetadata = sessions[index];
|
||||
final currentSession = mClient.getSessionInfo(currentSessionMetadata.sessionHash);
|
||||
if (currentSession == null) return null;
|
||||
return SessionTile(session: currentSession);
|
||||
},
|
||||
),
|
||||
AnimatedOpacity(
|
||||
opacity: _shevronOpacity,
|
||||
|
|
|
@ -6,128 +6,6 @@ import 'package:contacts_plus_plus/widgets/generic_avatar.dart';
|
|||
import 'package:contacts_plus_plus/widgets/sessions/session_view.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class SessionPopup extends StatelessWidget {
|
||||
const SessionPopup({required this.session, super.key});
|
||||
|
||||
final Session session;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final ScrollController userListScrollController = ScrollController();
|
||||
final thumbnailUri = Aux.resdbToHttp(session.thumbnailUrl);
|
||||
return Dialog(
|
||||
insetPadding: const EdgeInsets.all(32),
|
||||
child: Container(
|
||||
constraints: const BoxConstraints(maxHeight: 400),
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Expanded(
|
||||
child: ListView(
|
||||
children: [
|
||||
FormattedText(session.formattedName, style: Theme.of(context).textTheme.titleMedium),
|
||||
session.formattedDescription.isEmpty
|
||||
? const Text("No description")
|
||||
: FormattedText(session.formattedDescription,
|
||||
style: Theme.of(context).textTheme.labelMedium),
|
||||
Text(
|
||||
"Tags: ${session.tags.isEmpty ? "None" : session.tags.join(", ")}",
|
||||
style: Theme.of(context).textTheme.labelMedium,
|
||||
softWrap: true,
|
||||
),
|
||||
Text("Access: ${session.accessLevel.toReadableString()}",
|
||||
style: Theme.of(context).textTheme.labelMedium),
|
||||
Text("Users: ${session.sessionUsers.length}", style: Theme.of(context).textTheme.labelMedium),
|
||||
Text("Maximum users: ${session.maxUsers}", style: Theme.of(context).textTheme.labelMedium),
|
||||
Text("Headless: ${session.headlessHost ? "Yes" : "No"}",
|
||||
style: Theme.of(context).textTheme.labelMedium),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (session.sessionUsers.isNotEmpty)
|
||||
Expanded(
|
||||
child: Scrollbar(
|
||||
trackVisibility: true,
|
||||
controller: userListScrollController,
|
||||
thumbVisibility: true,
|
||||
child: ListView.builder(
|
||||
controller: userListScrollController,
|
||||
shrinkWrap: true,
|
||||
itemCount: session.sessionUsers.length,
|
||||
itemBuilder: (context, index) {
|
||||
final user = session.sessionUsers[index];
|
||||
return ListTile(
|
||||
dense: true,
|
||||
title: Text(
|
||||
user.username,
|
||||
textAlign: TextAlign.end,
|
||||
),
|
||||
subtitle: Text(
|
||||
user.isPresent ? "Active" : "Inactive",
|
||||
textAlign: TextAlign.end,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
)
|
||||
else
|
||||
const Expanded(
|
||||
child: Center(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(Icons.person_remove_alt_1_rounded),
|
||||
Padding(
|
||||
padding: EdgeInsets.all(16.0),
|
||||
child: Text(
|
||||
"No one is currently playing.",
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Center(
|
||||
child: CachedNetworkImage(
|
||||
imageUrl: thumbnailUri,
|
||||
placeholder: (context, url) {
|
||||
return const CircularProgressIndicator();
|
||||
},
|
||||
errorWidget: (context, error, what) => const Column(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(Icons.no_photography),
|
||||
Padding(
|
||||
padding: EdgeInsets.all(16.0),
|
||||
child: Text("Failed to load Image"),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class SessionTile extends StatelessWidget {
|
||||
const SessionTile({required this.session, super.key});
|
||||
|
||||
|
|
Loading…
Reference in a new issue