Merge pull request #1 from Nutcake/resonite

Resonite
This commit is contained in:
Nutcake 2023-10-03 19:52:17 +02:00 committed by GitHub
commit 9666ffa9be
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
126 changed files with 1207 additions and 1254 deletions

View file

@ -1,10 +1,10 @@
<img src="https://github.com/Nutcake/contacts-plus-plus/raw/main/assets/images/logo512.png" width="200"/> <img src="https://github.com/Nutcake/Recon/raw/main/assets/images/logo512.png" width="200"/>
# Contacts++ # ReCon
Messenger App for Neos VR contacts. A Resonite Contacts App for Android
[Get it here](https://github.com/Nutcake/contacts-plus-plus/releases/latest) [Get it here](https://github.com/Nutcake/ReCon/releases/latest)
## Building ## Building
@ -17,4 +17,4 @@ For example, voice-messages and notifications are currently not supported on des
## Screenshots ## Screenshots
<img src="https://cdn.discordapp.com/attachments/897112742035615804/1122142234905686047/Screenshot_20230624-143102_Contacts.png" width=198/> <img src="https://cdn.discordapp.com/attachments/897112742035615804/1122142235169923202/Screenshot_20230624-143035_Contacts.png" width=198/> <img src="https://cdn.discordapp.com/attachments/897112742035615804/1122142233773219890/Screenshot_20230624-143109_Contacts.png" width=198/> <img src="https://cdn.discordapp.com/attachments/897112742035615804/1122142233114726410/Screenshot_20230624-143205_Contacts.png" width=198/> <img src="https://cdn.discordapp.com/attachments/897112742035615804/1122142233458651209/Screenshot_20230624-143124_Contacts.png" width=198/> <img src="https://github.com/Nutcake/ReCon/assets/10452593/a46ccf8a-0a9f-4518-98e6-84fad2d7bf26" width=198/> <img src="https://github.com/Nutcake/ReCon/assets/10452593/5d158f58-cd27-4a68-abf3-9068e92b6a82" width=198/> <img src="https://github.com/Nutcake/ReCon/assets/10452593/f2ce95ef-e513-46cb-9654-31e74cdc7c09" width=198/> <img src="https://github.com/Nutcake/ReCon/assets/10452593/58ef5e5e-1b53-4a47-92f8-bcbcba7a1e86" width=198/>

View file

@ -49,7 +49,7 @@ android {
} }
defaultConfig { defaultConfig {
applicationId "me.voidspace.contacts_plus_plus" applicationId "me.voidspace.recon"
minSdkVersion 21 minSdkVersion 21
targetSdkVersion flutter.targetSdkVersion targetSdkVersion flutter.targetSdkVersion
versionCode flutterVersionCode.toInteger() versionCode flutterVersionCode.toInteger()

View file

@ -1,5 +1,5 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="de.voidspace.contacts_plus_plus"> package="de.voidspace.recon">
<!-- The INTERNET permission is required for development. Specifically, <!-- The INTERNET permission is required for development. Specifically,
the Flutter tool needs it to communicate with the running application the Flutter tool needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc. to allow setting breakpoints, to provide hot reload, etc.

View file

@ -1,5 +1,5 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="de.voidspace.contacts_plus_plus"> package="de.voidspace.recon">
<!-- Required to fetch data from the internet. --> <!-- Required to fetch data from the internet. -->
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
@ -7,7 +7,7 @@
<!-- Optional, you'll have to check this permission by yourself. --> <!-- Optional, you'll have to check this permission by yourself. -->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<application <application
android:label="Contacts++" android:label="ReCon"
android:name="${applicationName}" android:name="${applicationName}"
android:icon="@mipmap/ic_launcher"> android:icon="@mipmap/ic_launcher">
<activity <activity

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 54 KiB

View file

@ -1,4 +1,4 @@
package de.voidspace.contacts_plus_plus package de.voidspace.recon
import io.flutter.embedding.android.FlutterActivity import io.flutter.embedding.android.FlutterActivity

Binary file not shown.

Before

Width:  |  Height:  |  Size: 677 B

After

Width:  |  Height:  |  Size: 913 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 461 B

After

Width:  |  Height:  |  Size: 542 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 895 B

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 3 KiB

View file

@ -2,5 +2,4 @@
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android"> <adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/> <background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground"/> <foreground android:drawable="@mipmap/ic_launcher_foreground"/>
<monochrome android:drawable="@mipmap/ic_launcher_mono" />
</adaptive-icon> </adaptive-icon>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View file

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<color name="ic_launcher_background">#232426</color> <color name="ic_launcher_background">#050505</color>
</resources> </resources>

View file

@ -1,5 +1,5 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="de.voidspace.contacts_plus_plus"> package="de.voidspace.recon">
<!-- The INTERNET permission is required for development. Specifically, <!-- The INTERNET permission is required for development. Specifically,
the Flutter tool needs it to communicate with the running application the Flutter tool needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc. to allow setting breakpoints, to provide hot reload, etc.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 122 KiB

After

Width:  |  Height:  |  Size: 150 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 64 KiB

View file

@ -1,7 +1,7 @@
import 'dart:convert'; import 'dart:convert';
import 'package:contacts_plus_plus/clients/api_client.dart'; import 'package:recon/clients/api_client.dart';
import 'package:contacts_plus_plus/models/cloud_variable.dart'; import 'package:recon/models/cloud_variable.dart';
class CloudVariableApi { class CloudVariableApi {
static Future<CloudVariable> readCloudVariable(ApiClient client, static Future<CloudVariable> readCloudVariable(ApiClient client,

37
lib/apis/contact_api.dart Normal file
View file

@ -0,0 +1,37 @@
import 'dart:convert';
import 'package:recon/clients/api_client.dart';
import 'package:recon/models/users/friend.dart';
import 'package:recon/models/users/friend_status.dart';
import 'package:recon/models/users/user.dart';
import 'package:recon/models/users/user_profile.dart';
import 'package:recon/models/users/user_status.dart';
class ContactApi {
static Future<List<Friend>> getFriendsList(ApiClient client, {DateTime? lastStatusUpdate}) async {
final response = await client.get("/users/${client.userId}/contacts${lastStatusUpdate != null ? "?lastStatusUpdate=${lastStatusUpdate.toUtc().toIso8601String()}" : ""}");
client.checkResponse(response);
final data = jsonDecode(response.body) as List;
return data.map((e) => Friend.fromMap(e)).toList();
}
static Future<void> addUserAsFriend(ApiClient client, {required User user}) async {
final friend = Friend(
id: user.id,
username: user.username,
ownerId: client.userId,
userStatus: UserStatus.empty(),
userProfile: UserProfile.empty(),
contactStatus: FriendStatus.accepted,
latestMessageTime: DateTime.now(),
);
final body = jsonEncode(friend.toMap(shallow: true));
final response = await client.put("/users/${client.userId}/contacts/${user.id}", body: body);
client.checkResponse(response);
}
static Future<void> removeUserAsFriend(ApiClient client, {required User user}) async {
final response = await client.delete("/users/${client.userId}/friends/${user.id}");
client.checkResponse(response);
}
}

View file

@ -1,13 +0,0 @@
import 'dart:convert';
import 'package:contacts_plus_plus/clients/api_client.dart';
import 'package:contacts_plus_plus/models/users/friend.dart';
class FriendApi {
static Future<List<Friend>> getFriendsList(ApiClient client, {DateTime? lastStatusUpdate}) async {
final response = await client.get("/users/${client.userId}/friends${lastStatusUpdate != null ? "?lastStatusUpdate=${lastStatusUpdate.toUtc().toIso8601String()}" : ""}");
client.checkResponse(response);
final data = jsonDecode(response.body) as List;
return data.map((e) => Friend.fromMap(e)).toList();
}
}

View file

@ -6,7 +6,7 @@ class GithubApi {
static const baseUrl = "https://api.github.com"; static const baseUrl = "https://api.github.com";
static Future<String> getLatestTagName() async { static Future<String> getLatestTagName() async {
final response = await http.get(Uri.parse("$baseUrl/repos/Nutcake/contacts-plus-plus/releases/latest")); final response = await http.get(Uri.parse("$baseUrl/repos/Nutcake/ReCon/releases/latest"));
if (response.statusCode != 200) return ""; if (response.statusCode != 200) return "";
final body = jsonDecode(response.body); final body = jsonDecode(response.body);
return body["tag_name"] ?? ""; return body["tag_name"] ?? "";

View file

@ -1,7 +1,7 @@
import 'dart:convert'; import 'dart:convert';
import 'package:contacts_plus_plus/clients/api_client.dart'; import 'package:recon/clients/api_client.dart';
import 'package:contacts_plus_plus/models/message.dart'; import 'package:recon/models/message.dart';
class MessageApi { class MessageApi {
static Future<List<Message>> getUserMessages(ApiClient client, {String userId = "", DateTime? fromTime, static Future<List<Message>> getUserMessages(ApiClient client, {String userId = "", DateTime? fromTime,

View file

@ -3,16 +3,16 @@ import 'dart:io';
import 'dart:math'; import 'dart:math';
import 'dart:typed_data'; import 'dart:typed_data';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:contacts_plus_plus/models/records/asset_digest.dart'; import 'package:recon/models/records/asset_digest.dart';
import 'package:contacts_plus_plus/models/records/json_template.dart'; import 'package:recon/models/records/json_template.dart';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:contacts_plus_plus/clients/api_client.dart'; import 'package:recon/clients/api_client.dart';
import 'package:contacts_plus_plus/models/records/asset_upload_data.dart'; import 'package:recon/models/records/asset_upload_data.dart';
import 'package:contacts_plus_plus/models/records/neos_db_asset.dart'; import 'package:recon/models/records/resonite_db_asset.dart';
import 'package:contacts_plus_plus/models/records/preprocess_status.dart'; import 'package:recon/models/records/preprocess_status.dart';
import 'package:contacts_plus_plus/models/records/record.dart'; import 'package:recon/models/records/record.dart';
import 'package:http_parser/http_parser.dart'; import 'package:http_parser/http_parser.dart';
import 'package:path/path.dart'; import 'package:path/path.dart';
@ -68,7 +68,7 @@ class RecordApi {
return status; return status;
} }
static Future<AssetUploadData> beginUploadAsset(ApiClient client, {required NeosDBAsset asset}) async { static Future<AssetUploadData> beginUploadAsset(ApiClient client, {required ResoniteDBAsset asset}) async {
final response = await client.post("/users/${client.userId}/assets/${asset.hash}/chunks"); final response = await client.post("/users/${client.userId}/assets/${asset.hash}/chunks");
client.checkResponse(response); client.checkResponse(response);
final body = jsonDecode(response.body); final body = jsonDecode(response.body);
@ -84,7 +84,7 @@ class RecordApi {
} }
static Future<void> uploadAsset(ApiClient client, static Future<void> uploadAsset(ApiClient client,
{required AssetUploadData uploadData, required String filename, required NeosDBAsset asset, required Uint8List data, void Function(double number)? progressCallback}) async { {required AssetUploadData uploadData, required String filename, required ResoniteDBAsset asset, required Uint8List data, void Function(double number)? progressCallback}) async {
for (int i = 0; i < uploadData.totalChunks; i++) { for (int i = 0; i < uploadData.totalChunks; i++) {
progressCallback?.call(i/uploadData.totalChunks); progressCallback?.call(i/uploadData.totalChunks);
final offset = i * uploadData.chunkSize; final offset = i * uploadData.chunkSize;
@ -104,7 +104,7 @@ class RecordApi {
} }
} }
static Future<void> finishUpload(ApiClient client, {required NeosDBAsset asset}) async { static Future<void> finishUpload(ApiClient client, {required ResoniteDBAsset asset}) async {
final response = await client.patch("/users/${client.userId}/assets/${asset.hash}/chunks"); final response = await client.patch("/users/${client.userId}/assets/${asset.hash}/chunks");
client.checkResponse(response); client.checkResponse(response);
} }

View file

@ -1,7 +1,7 @@
import 'dart:convert'; import 'dart:convert';
import 'package:contacts_plus_plus/clients/api_client.dart'; import 'package:recon/clients/api_client.dart';
import 'package:contacts_plus_plus/models/session.dart'; import 'package:recon/models/session.dart';
class SessionApi { class SessionApi {
static Future<Session> getSession(ApiClient client, {required String sessionId}) async { static Future<Session> getSession(ApiClient client, {required String sessionId}) async {

View file

@ -1,13 +1,9 @@
import 'dart:convert'; import 'dart:convert';
import 'package:contacts_plus_plus/clients/api_client.dart'; import 'package:recon/clients/api_client.dart';
import 'package:contacts_plus_plus/models/users/friend.dart'; import 'package:recon/models/personal_profile.dart';
import 'package:contacts_plus_plus/models/personal_profile.dart'; import 'package:recon/models/users/user.dart';
import 'package:contacts_plus_plus/models/users/user.dart'; import 'package:recon/models/users/user_status.dart';
import 'package:contacts_plus_plus/models/users/user_profile.dart';
import 'package:contacts_plus_plus/models/users/friend_status.dart';
import 'package:contacts_plus_plus/models/users/user_status.dart';
import 'package:package_info_plus/package_info_plus.dart';
class UserApi { class UserApi {
static Future<Iterable<User>> searchUsers(ApiClient client, {required String needle}) async { static Future<Iterable<User>> searchUsers(ApiClient client, {required String needle}) async {
@ -25,6 +21,7 @@ class UserApi {
} }
static Future<UserStatus> getUserStatus(ApiClient client, {required String userId}) async { static Future<UserStatus> getUserStatus(ApiClient client, {required String userId}) async {
return UserStatus.empty();
final response = await client.get("/users/$userId/status"); final response = await client.get("/users/$userId/status");
client.checkResponse(response); client.checkResponse(response);
final data = jsonDecode(response.body); final data = jsonDecode(response.body);
@ -32,18 +29,7 @@ class UserApi {
} }
static Future<void> notifyOnlineInstance(ApiClient client) async { static Future<void> notifyOnlineInstance(ApiClient client) async {
final response = await client.post("/stats/instanceOnline/${client.authenticationData.secretMachineId.hashCode}"); final response = await client.post("/stats/instanceOnline/${client.authenticationData.secretMachineIdHash}");
client.checkResponse(response);
}
static Future<void> setStatus(ApiClient client, {required UserStatus status}) async {
final pkginfo = await PackageInfo.fromPlatform();
status = status.copyWith(
neosVersion: "${pkginfo.version} of ${pkginfo.appName}",
isMobile: true,
);
final body = jsonEncode(status.toMap(shallow: true));
final response = await client.put("/users/${client.userId}/status", body: body);
client.checkResponse(response); client.checkResponse(response);
} }
@ -53,24 +39,4 @@ class UserApi {
final data = jsonDecode(response.body); final data = jsonDecode(response.body);
return PersonalProfile.fromMap(data); return PersonalProfile.fromMap(data);
} }
static Future<void> addUserAsFriend(ApiClient client, {required User user}) async {
final friend = Friend(
id: user.id,
username: user.username,
ownerId: client.userId,
userStatus: UserStatus.empty(),
userProfile: UserProfile.empty(),
friendStatus: FriendStatus.accepted,
latestMessageTime: DateTime.now(),
);
final body = jsonEncode(friend.toMap(shallow: true));
final response = await client.put("/users/${client.userId}/friends/${user.id}", body: body);
client.checkResponse(response);
}
static Future<void> removeUserAsFriend(ApiClient client, {required User user}) async {
final response = await client.delete("/users/${client.userId}/friends/${user.id}");
client.checkResponse(response);
}
} }

View file

@ -1,55 +1,14 @@
import 'package:contacts_plus_plus/config.dart'; import 'package:recon/config.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:path/path.dart' as p; import 'package:path/path.dart' as p;
import 'package:html/parser.dart' as htmlparser; import 'package:html/parser.dart' as htmlparser;
enum NeosDBEndpoint
{
def,
blob,
cdn,
videoCDN,
}
extension NeosStringExtensions on Uri {
static String dbSignature(Uri neosdb) => neosdb.pathSegments.length < 2 ? "" : p.basenameWithoutExtension(neosdb.pathSegments[1]);
static String? neosDBQuery(Uri neosdb) => neosdb.query.trim().isEmpty ? null : neosdb.query.substring(1);
static bool isLegacyNeosDB(Uri uri) => !(uri.scheme != "neosdb") && uri.pathSegments.length >= 2 && p.basenameWithoutExtension(uri.pathSegments[1]).length < 30;
Uri neosDBToHTTP(NeosDBEndpoint endpoint) {
var signature = dbSignature(this);
var query = neosDBQuery(this);
if (query != null) {
signature = "$signature/$query";
}
if (isLegacyNeosDB(this)) {
return Uri.parse(Config.legacyCloudUrl + signature);
}
String base;
switch (endpoint) {
case NeosDBEndpoint.blob:
base = Config.blobStorageUrl;
break;
case NeosDBEndpoint.cdn:
base = Config.neosCdnUrl;
break;
case NeosDBEndpoint.videoCDN:
base = Config.videoStorageUrl;
break;
case NeosDBEndpoint.def:
base = Config.neosAssetsUrl;
}
return Uri.parse(base + signature);
}
}
class Aux { class Aux {
static String neosDbToHttp(String? neosdb) { static String resdbToHttp(String? resdb) {
if (neosdb == null || neosdb.isEmpty) return ""; if (resdb == null || resdb.isEmpty) return "";
if (neosdb.startsWith("http")) return neosdb; if (resdb.startsWith("http")) return resdb;
final filename = p.basenameWithoutExtension(neosdb); final filename = p.basenameWithoutExtension(resdb);
return "${Config.neosCdnUrl}$filename"; return "${Config.skyfrostAssetsUrl}/$filename";
} }
} }

View file

@ -1,8 +1,8 @@
import 'package:contacts_plus_plus/clients/api_client.dart'; import 'package:recon/clients/api_client.dart';
import 'package:contacts_plus_plus/clients/notification_client.dart'; import 'package:recon/clients/notification_client.dart';
import 'package:contacts_plus_plus/clients/settings_client.dart'; import 'package:recon/clients/settings_client.dart';
import 'package:contacts_plus_plus/models/authentication_data.dart'; import 'package:recon/models/authentication_data.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
class ClientHolder extends InheritedWidget { class ClientHolder extends InheritedWidget {

View file

@ -3,7 +3,7 @@ import 'dart:convert';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
import 'package:contacts_plus_plus/models/authentication_data.dart'; import 'package:recon/models/authentication_data.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
import 'package:uuid/uuid.dart'; import 'package:uuid/uuid.dart';
@ -15,6 +15,7 @@ class ApiClient {
static const String machineIdKey = "machineId"; static const String machineIdKey = "machineId";
static const String tokenKey = "token"; static const String tokenKey = "token";
static const String passwordKey = "password"; static const String passwordKey = "password";
static const String uidKey = "uid";
ApiClient({required AuthenticationData authenticationData, required this.onLogout}) ApiClient({required AuthenticationData authenticationData, required this.onLogout})
: _authenticationData = authenticationData; : _authenticationData = authenticationData;
@ -41,14 +42,19 @@ class ApiClient {
}) async { }) async {
final body = { final body = {
(username.contains("@") ? "email" : "username"): username.trim(), (username.contains("@") ? "email" : "username"): username.trim(),
"password": password, "authentication": {
"\$type": "password",
"password": password,
},
"rememberMe": rememberMe, "rememberMe": rememberMe,
"secretMachineId": const Uuid().v4(), "secretMachineId": const Uuid().v4(),
}; };
final uid = const Uuid().v4().replaceAll("-", "");
final response = await http.post( final response = await http.post(
buildFullUri("/UserSessions"), buildFullUri("/userSessions"),
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
"UID": uid,
if (oneTimePad != null) totpKey: oneTimePad, if (oneTimePad != null) totpKey: oneTimePad,
}, },
body: jsonEncode(body), body: jsonEncode(body),
@ -60,15 +66,17 @@ class ApiClient {
throw "Invalid Credentials"; throw "Invalid Credentials";
} }
checkResponseCode(response); checkResponseCode(response);
final data = jsonDecode(response.body);
final authData = AuthenticationData.fromMap(jsonDecode(response.body)); data["entity"]["uid"] = uid;
final authData = AuthenticationData.fromMap(data);
if (authData.isAuthenticated) { if (authData.isAuthenticated) {
const FlutterSecureStorage storage = FlutterSecureStorage( const FlutterSecureStorage storage = FlutterSecureStorage(
aOptions: AndroidOptions(encryptedSharedPreferences: true), aOptions: AndroidOptions(encryptedSharedPreferences: true),
); );
await storage.write(key: userIdKey, value: authData.userId); await storage.write(key: userIdKey, value: authData.userId);
await storage.write(key: machineIdKey, value: authData.secretMachineId); await storage.write(key: machineIdKey, value: authData.secretMachineIdHash);
await storage.write(key: tokenKey, value: authData.token); await storage.write(key: tokenKey, value: authData.token);
await storage.write(key: uidKey, value: authData.uid);
if (rememberPass) await storage.write(key: passwordKey, value: password); if (rememberPass) await storage.write(key: passwordKey, value: password);
} }
return authData; return authData;
@ -82,16 +90,25 @@ class ApiClient {
String? machineId = await storage.read(key: machineIdKey); String? machineId = await storage.read(key: machineIdKey);
String? token = await storage.read(key: tokenKey); String? token = await storage.read(key: tokenKey);
String? password = await storage.read(key: passwordKey); String? password = await storage.read(key: passwordKey);
String? uid = await storage.read(key: uidKey);
if (userId == null || machineId == null) { if (userId == null || machineId == null || uid == null) {
return AuthenticationData.unauthenticated(); return AuthenticationData.unauthenticated();
} }
if (token != null) { if (token != null) {
final response = final response = await http.patch(buildFullUri("/userSessions"), headers: {
await http.patch(buildFullUri("/userSessions"), headers: {"Authorization": "neos $userId:$token"}); "Authorization": "res $userId:$token",
"UID": uid,
});
if (response.statusCode < 300) { if (response.statusCode < 300) {
return AuthenticationData(userId: userId, token: token, secretMachineId: machineId, isAuthenticated: true); return AuthenticationData(
userId: userId,
token: token,
secretMachineIdHash: machineId,
isAuthenticated: true,
uid: uid,
);
} }
} }
@ -148,13 +165,16 @@ class ApiClient {
_ => "Unknown Error." _ => "Unknown Error."
}} (${response.statusCode}${kDebugMode && response.body.isNotEmpty ? "|${response.body}" : ""})"; }} (${response.statusCode}${kDebugMode && response.body.isNotEmpty ? "|${response.body}" : ""})";
FlutterError.reportError(FlutterErrorDetails(exception: error)); FlutterError.reportError(FlutterErrorDetails(
exception: error,
stack: StackTrace.current,
));
throw error; throw error;
} }
Map<String, String> get authorizationHeader => _authenticationData.authorizationHeader; Map<String, String> get authorizationHeader => _authenticationData.authorizationHeader;
static Uri buildFullUri(String path) => Uri.parse("${Config.apiBaseUrl}/api$path"); static Uri buildFullUri(String path) => Uri.parse("${Config.apiBaseUrl}$path");
Future<http.Response> get(String path, {Map<String, String>? headers}) async { Future<http.Response> get(String path, {Map<String, String>? headers}) async {
headers ??= {}; headers ??= {};

View file

@ -1,9 +1,9 @@
import 'dart:io'; import 'dart:io';
import 'package:contacts_plus_plus/auxiliary.dart'; import 'package:recon/auxiliary.dart';
import 'package:contacts_plus_plus/clients/api_client.dart'; import 'package:recon/clients/api_client.dart';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
import 'package:contacts_plus_plus/models/message.dart'; import 'package:recon/models/message.dart';
import 'package:path/path.dart'; import 'package:path/path.dart';
import 'package:path_provider/path_provider.dart'; import 'package:path_provider/path_provider.dart';
@ -15,7 +15,7 @@ class AudioCacheClient {
final file = File("${directory.path}/${basename(clip.assetUri)}"); final file = File("${directory.path}/${basename(clip.assetUri)}");
if (!await file.exists()) { if (!await file.exists()) {
await file.create(recursive: true); await file.create(recursive: true);
final response = await http.get(Uri.parse(Aux.neosDbToHttp(clip.assetUri))); final response = await http.get(Uri.parse(Aux.resdbToHttp(clip.assetUri)));
ApiClient.checkResponseCode(response); ApiClient.checkResponseCode(response);
await file.writeAsBytes(response.bodyBytes); await file.writeAsBytes(response.bodyBytes);
} }

View file

@ -1,17 +1,17 @@
import 'dart:async'; import 'dart:async';
import 'package:contacts_plus_plus/apis/record_api.dart'; import 'package:recon/apis/record_api.dart';
import 'package:contacts_plus_plus/clients/api_client.dart'; import 'package:recon/clients/api_client.dart';
import 'package:contacts_plus_plus/models/inventory/neos_path.dart'; import 'package:recon/models/inventory/resonite_directory.dart';
import 'package:contacts_plus_plus/models/records/record.dart'; import 'package:recon/models/records/record.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
class InventoryClient extends ChangeNotifier { class InventoryClient extends ChangeNotifier {
final ApiClient apiClient; final ApiClient apiClient;
Future<NeosDirectory>? _currentDirectory; Future<ResoniteDirectory>? _currentDirectory;
Future<NeosDirectory>? get directoryFuture => _currentDirectory; Future<ResoniteDirectory>? get directoryFuture => _currentDirectory;
InventoryClient({required this.apiClient}); InventoryClient({required this.apiClient});
@ -51,7 +51,7 @@ class InventoryClient extends ChangeNotifier {
} }
Future<List<Record>> _getDirectory(Record record) async { Future<List<Record>> _getDirectory(Record record) async {
NeosDirectory? dir; ResoniteDirectory? dir;
try { try {
dir = await _currentDirectory; dir = await _currentDirectory;
} catch (_) {} } catch (_) {}
@ -59,7 +59,7 @@ class InventoryClient extends ChangeNotifier {
if (dir == null || record.isRoot) { if (dir == null || record.isRoot) {
records = await RecordApi.getUserRecordsAt( records = await RecordApi.getUserRecordsAt(
apiClient, apiClient,
path: NeosDirectory.rootName, path: ResoniteDirectory.rootName,
); );
} else { } else {
if (record.recordType == RecordType.link) { if (record.recordType == RecordType.link) {
@ -79,12 +79,12 @@ class InventoryClient extends ChangeNotifier {
final rootRecord = Record.inventoryRoot(); final rootRecord = Record.inventoryRoot();
final rootFuture = _getDirectory(rootRecord).then( final rootFuture = _getDirectory(rootRecord).then(
(records) { (records) {
final rootDir = NeosDirectory( final rootDir = ResoniteDirectory(
record: rootRecord, record: rootRecord,
children: [], children: [],
); );
rootDir.children.addAll( rootDir.children.addAll(
records.map((e) => NeosDirectory.fromRecord(record: e, parent: rootDir)).toList(), records.map((e) => ResoniteDirectory.fromRecord(record: e, parent: rootDir)).toList(),
); );
return rootDir; return rootDir;
}, },
@ -103,8 +103,8 @@ class InventoryClient extends ChangeNotifier {
_currentDirectory = _getDirectory(dir.record).then( _currentDirectory = _getDirectory(dir.record).then(
(records) { (records) {
final children = records.map((record) => NeosDirectory.fromRecord(record: record, parent: dir)).toList(); final children = records.map((record) => ResoniteDirectory.fromRecord(record: record, parent: dir)).toList();
final newDir = NeosDirectory(record: dir.record, children: children, parent: dir.parent); final newDir = ResoniteDirectory(record: dir.record, children: children, parent: dir.parent);
final parentIdx = dir.parent?.children.indexOf(dir) ?? -1; final parentIdx = dir.parent?.children.indexOf(dir) ?? -1;
if (parentIdx != -1) { if (parentIdx != -1) {
@ -142,7 +142,7 @@ class InventoryClient extends ChangeNotifier {
_currentDirectory = _getDirectory(record).then( _currentDirectory = _getDirectory(record).then(
(records) { (records) {
childDir.children.clear(); childDir.children.clear();
childDir.children.addAll(records.map((record) => NeosDirectory.fromRecord(record: record, parent: childDir))); childDir.children.addAll(records.map((record) => ResoniteDirectory.fromRecord(record: record, parent: childDir)));
return childDir; return childDir;
}, },
).onError((error, stackTrace) { ).onError((error, stackTrace) {

View file

@ -1,50 +1,25 @@
import 'dart:async'; import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:recon/apis/contact_api.dart';
import 'package:recon/apis/message_api.dart';
import 'package:recon/apis/user_api.dart';
import 'package:recon/clients/api_client.dart';
import 'package:recon/clients/notification_client.dart';
import 'package:recon/crypto_helper.dart';
import 'package:recon/hub_manager.dart';
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/user_status.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:hive_flutter/hive_flutter.dart'; import 'package:hive_flutter/hive_flutter.dart';
import 'package:http/http.dart' as http;
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:contacts_plus_plus/apis/friend_api.dart';
import 'package:contacts_plus_plus/apis/message_api.dart';
import 'package:contacts_plus_plus/apis/user_api.dart';
import 'package:contacts_plus_plus/clients/notification_client.dart';
import 'package:contacts_plus_plus/models/users/friend.dart';
import 'package:contacts_plus_plus/clients/api_client.dart';
import 'package:contacts_plus_plus/config.dart';
import 'package:contacts_plus_plus/models/message.dart';
enum EventType {
unknown,
message,
unknown1,
unknown2,
unknown3,
unknown4,
keepAlive,
error;
}
enum EventTarget {
unknown,
messageSent,
receiveMessage,
messagesRead;
factory EventTarget.parse(String? text) {
if (text == null) return EventTarget.unknown;
return EventTarget.values.firstWhere((element) => element.name.toLowerCase() == text.toLowerCase(),
orElse: () => EventTarget.unknown,
);
}
}
class MessagingClient extends ChangeNotifier { class MessagingClient extends ChangeNotifier {
static const String _eofChar = "";
static const String _negotiationPacket = "{\"protocol\":\"json\", \"version\":1}$_eofChar";
static const List<int> _reconnectTimeoutsSeconds = [0, 5, 10, 20, 60];
static const Duration _autoRefreshDuration = Duration(seconds: 10); static const Duration _autoRefreshDuration = Duration(seconds: 10);
static const Duration _unreadSafeguardDuration = Duration(seconds: 120); static const Duration _unreadSafeguardDuration = Duration(seconds: 120);
static const String _messageBoxKey = "message-box"; static const String _messageBoxKey = "message-box";
@ -54,32 +29,27 @@ class MessagingClient extends ChangeNotifier {
final List<Friend> _sortedFriendsCache = []; // Keep a sorted copy so as to not have to sort during build() final List<Friend> _sortedFriendsCache = []; // Keep a sorted copy so as to not have to sort during build()
final Map<String, MessageCache> _messageCache = {}; final Map<String, MessageCache> _messageCache = {};
final Map<String, List<Message>> _unreads = {}; final Map<String, List<Message>> _unreads = {};
final Logger _logger = Logger("NeosHub"); final Logger _logger = Logger("Messaging");
final NotificationClient _notificationClient; final NotificationClient _notificationClient;
final HubManager _hubManager = HubManager();
final Map<String, Session> _sessionMap = {};
Friend? selectedFriend; Friend? selectedFriend;
Timer? _notifyOnlineTimer; Timer? _notifyOnlineTimer;
Timer? _autoRefresh; Timer? _autoRefresh;
Timer? _unreadSafeguard; Timer? _unreadSafeguard;
int _attempts = 0;
WebSocket? _wsChannel;
bool _isConnecting = false;
String? _initStatus; String? _initStatus;
UserStatus _userStatus = UserStatus.initial();
UserStatus get userStatus => _userStatus;
MessagingClient({required ApiClient apiClient, required NotificationClient notificationClient}) MessagingClient({required ApiClient apiClient, required NotificationClient notificationClient})
: _apiClient = apiClient, _notificationClient = notificationClient { : _apiClient = apiClient,
_notificationClient = notificationClient {
debugPrint("mClient created: $hashCode"); debugPrint("mClient created: $hashCode");
Hive.openBox(_messageBoxKey).then((box) async { Hive.openBox(_messageBoxKey).then((box) async {
box.delete(_lastUpdateKey); await box.delete(_lastUpdateKey);
await refreshFriendsListWithErrorHandler(); _setupHub();
await _refreshUnreads();
_unreadSafeguard = Timer.periodic(_unreadSafeguardDuration, (timer) => _refreshUnreads());
});
_startWebsocket();
_notifyOnlineTimer = Timer.periodic(const Duration(seconds: 60), (timer) async {
// We should probably let the MessagingClient handle the entire state of USerStatus instead of mirroring like this
// but I don't feel like implementing that right now.
UserApi.setStatus(apiClient, status: await UserApi.getUserStatus(apiClient, userId: apiClient.userId));
}); });
} }
@ -89,21 +59,20 @@ class MessagingClient extends ChangeNotifier {
_autoRefresh?.cancel(); _autoRefresh?.cancel();
_notifyOnlineTimer?.cancel(); _notifyOnlineTimer?.cancel();
_unreadSafeguard?.cancel(); _unreadSafeguard?.cancel();
_wsChannel?.close(); _hubManager.dispose();
super.dispose(); super.dispose();
} }
String? get initStatus => _initStatus; String? get initStatus => _initStatus;
bool get websocketConnected => _wsChannel != null;
List<Friend> get cachedFriends => _sortedFriendsCache; List<Friend> get cachedFriends => _sortedFriendsCache;
List<Message> getUnreadsForFriend(Friend friend) => _unreads[friend.id] ?? []; List<Message> getUnreadsForFriend(Friend friend) => _unreads[friend.id] ?? [];
bool friendHasUnreads(Friend friend) => _unreads.containsKey(friend.id); bool friendHasUnreads(Friend friend) => _unreads.containsKey(friend.id);
bool messageIsUnread(Message message) => _unreads[message.senderId]?.any((element) => element.id == message.id) ?? false; bool messageIsUnread(Message message) =>
_unreads[message.senderId]?.any((element) => element.id == message.id) ?? false;
Friend? getAsFriend(String userId) => Friend.fromMapOrNull(Hive.box(_messageBoxKey).get(userId)); Friend? getAsFriend(String userId) => Friend.fromMapOrNull(Hive.box(_messageBoxKey).get(userId));
@ -111,8 +80,7 @@ class MessagingClient extends ChangeNotifier {
MessageCache _createUserMessageCache(String userId) => MessageCache(apiClient: _apiClient, userId: userId); MessageCache _createUserMessageCache(String userId) => MessageCache(apiClient: _apiClient, userId: userId);
Future<void> refreshFriendsListWithErrorHandler() async {
Future<void> refreshFriendsListWithErrorHandler () async {
try { try {
await refreshFriendsList(); await refreshFriendsList();
} catch (e) { } catch (e) {
@ -126,25 +94,18 @@ class MessagingClient extends ChangeNotifier {
_autoRefresh?.cancel(); _autoRefresh?.cancel();
_autoRefresh = Timer(_autoRefreshDuration, () => refreshFriendsList()); _autoRefresh = Timer(_autoRefreshDuration, () => refreshFriendsList());
final friends = await FriendApi.getFriendsList(_apiClient, lastStatusUpdate: lastUpdateUtc); final friends = await ContactApi.getFriendsList(_apiClient, lastStatusUpdate: lastUpdateUtc);
for (final friend in friends) { for (final friend in friends) {
await _updateFriend(friend); await _updateContact(friend);
} }
_initStatus = ""; _initStatus = "";
notifyListeners(); notifyListeners();
} }
void sendMessage(Message message) async { void sendMessage(Message message) {
final msgBody = message.toMap(); final msgBody = message.toMap();
final data = { _hubManager.send("SendMessage", arguments: [msgBody]);
"type": EventType.message.index,
"target": "SendMessage",
"arguments": [
msgBody
],
};
_sendData(data);
final cache = getUserMessageCache(message.recipientId) ?? _createUserMessageCache(message.recipientId); final cache = getUserMessageCache(message.recipientId) ?? _createUserMessageCache(message.recipientId);
cache.addMessage(message); cache.addMessage(message);
notifyListeners(); notifyListeners();
@ -152,17 +113,37 @@ class MessagingClient extends ChangeNotifier {
void markMessagesRead(MarkReadBatch batch) { void markMessagesRead(MarkReadBatch batch) {
final msgBody = batch.toMap(); final msgBody = batch.toMap();
final data = { _hubManager.send("MarkMessagesRead", arguments: [msgBody]);
"type": EventType.message.index,
"target": "MarkMessagesRead",
"arguments": [
msgBody
],
};
_sendData(data);
clearUnreadsForUser(batch.senderId); clearUnreadsForUser(batch.senderId);
} }
Future<void> setUserStatus(UserStatus status) async {
final pkginfo = await PackageInfo.fromPlatform();
_userStatus = _userStatus.copyWith(
appVersion: "${pkginfo.version} of ${pkginfo.appName}",
lastStatusChange: DateTime.now(),
onlineStatus: status.onlineStatus,
);
_hubManager.send(
"BroadcastStatus",
arguments: [
_userStatus.toMap(),
{
"group": 1,
"targetIds": null,
}
],
);
final self = getAsFriend(_apiClient.userId);
if (self != null) {
await _updateContact(self.copyWith(userStatus: _userStatus));
}
notifyListeners();
}
void addUnread(Message message) { void addUnread(Message message) {
var messages = _unreads[message.senderId]; var messages = _unreads[message.senderId];
if (messages == null) { if (messages == null) {
@ -211,7 +192,7 @@ class MessagingClient extends ChangeNotifier {
final friend = getAsFriend(userId); final friend = getAsFriend(userId);
if (friend == null) return; if (friend == null) return;
final newStatus = await UserApi.getUserStatus(_apiClient, userId: userId); final newStatus = await UserApi.getUserStatus(_apiClient, userId: userId);
await _updateFriend(friend.copyWith(userStatus: newStatus)); await _updateContact(friend.copyWith(userStatus: newStatus));
notifyListeners(); notifyListeners();
} }
@ -238,7 +219,7 @@ class MessagingClient extends ChangeNotifier {
}); });
} }
Future<void> _updateFriend(Friend friend) async { Future<void> _updateContact(Friend friend) async {
final box = Hive.box(_messageBoxKey); final box = Hive.box(_messageBoxKey);
box.put(friend.id, friend.toMap()); box.put(friend.id, friend.toMap());
final lastStatusUpdate = box.get(_lastUpdateKey); final lastStatusUpdate = box.get(_lastUpdateKey);
@ -257,136 +238,100 @@ class MessagingClient extends ChangeNotifier {
_sortFriendsCache(); _sortFriendsCache();
} }
// ===== Websocket Stuff ===== Future<void> _setupHub() async {
void _onDisconnected(error) async {
_wsChannel = null;
_logger.warning("Neos Hub connection died with error '$error', reconnecting...");
await _startWebsocket();
}
Future<void> _startWebsocket() async {
if (!_apiClient.isAuthenticated) { if (!_apiClient.isAuthenticated) {
_logger.info("Tried to connect to Neos Hub without authentication, this is probably fine for now."); _logger.info("Tried to connect to Resonite Hub without authentication, this is probably fine for now.");
return; return;
} }
if (_isConnecting) { _hubManager.setHeaders(_apiClient.authorizationHeader);
return;
}
_isConnecting = true;
_wsChannel = await _tryConnect();
_isConnecting = false;
_logger.info("Connected to Neos Hub.");
_wsChannel!.done.then((error) => _onDisconnected(error));
_wsChannel!.listen(_handleEvent, onDone: () => _onDisconnected("Connection closed."), onError: _onDisconnected);
_wsChannel!.add(_negotiationPacket);
}
Future<WebSocket> _tryConnect() async { _hubManager.setHandler(EventTarget.messageSent, _onMessageSent);
while (true) { _hubManager.setHandler(EventTarget.receiveMessage, _onReceiveMessage);
try { _hubManager.setHandler(EventTarget.messagesRead, _onMessagesRead);
final http.Response response; _hubManager.setHandler(EventTarget.receiveStatusUpdate, _onReceiveStatusUpdate);
try { _hubManager.setHandler(EventTarget.receiveSessionUpdate, _onReceiveSessionUpdate);
response = await http.post( _hubManager.setHandler(EventTarget.removeSession, _onRemoveSession);
Uri.parse("${Config.neosHubUrl}/negotiate"),
headers: _apiClient.authorizationHeader, await _hubManager.start();
); await setUserStatus(userStatus);
_apiClient.checkResponse(response); _hubManager.send(
} catch (e) { "InitializeStatus",
throw "Failed to acquire connection info from Neos API: $e"; responseHandler: (Map data) async {
final rawContacts = data["contacts"] as List;
final contacts = rawContacts.map((e) => Friend.fromMap(e)).toList();
for (final contact in contacts) {
await _updateContact(contact);
} }
final body = jsonDecode(response.body); _initStatus = "";
final url = (body["url"] as String?)?.replaceFirst("https://", "wss://");
final wsToken = body["accessToken"];
if (url == null || wsToken == null) {
throw "Invalid response from server.";
}
final ws = await WebSocket.connect("$url&access_token=$wsToken");
_attempts = 0;
return ws;
} catch (e) {
final timeout = _reconnectTimeoutsSeconds[_attempts.clamp(0, _reconnectTimeoutsSeconds.length - 1)];
_logger.severe(e);
_logger.severe("Retrying in $timeout seconds");
await Future.delayed(Duration(seconds: timeout));
_attempts++;
}
}
}
void _handleEvent(event) {
final body = jsonDecode((event.toString().replaceAll(_eofChar, "")));
final int rawType = body["type"] ?? 0;
if (rawType > EventType.values.length) {
_logger.info("Unhandled event type $rawType: $body");
return;
}
switch (EventType.values[rawType]) {
case EventType.unknown1:
case EventType.unknown2:
case EventType.unknown3:
case EventType.unknown4:
case EventType.unknown:
_logger.info("Received unknown event: $rawType: $body");
break;
case EventType.message:
_logger.info("Received message-event.");
_handleMessageEvent(body);
break;
case EventType.keepAlive:
_logger.info("Received keep-alive.");
break;
case EventType.error:
_logger.severe("Received error-event: ${body["error"]}");
// Should we trigger a manual reconnect here or just let the remote service close the connection?
break;
}
}
void _handleMessageEvent(body) async {
final target = EventTarget.parse(body["target"]);
final args = body["arguments"];
switch (target) {
case EventTarget.unknown:
_logger.info("Unknown event-target in message: $body");
return;
case EventTarget.messageSent:
final msg = args[0];
final message = Message.fromMap(msg, withState: MessageState.sent);
final cache = getUserMessageCache(message.recipientId) ?? _createUserMessageCache(message.recipientId);
cache.addMessage(message);
notifyListeners(); notifyListeners();
break; await _refreshUnreads();
case EventTarget.receiveMessage: _unreadSafeguard = Timer.periodic(_unreadSafeguardDuration, (timer) => _refreshUnreads());
final msg = args[0]; _hubManager.send("RequestStatus", arguments: [null, false]);
final message = Message.fromMap(msg); },
final cache = getUserMessageCache(message.senderId) ?? _createUserMessageCache(message.senderId); );
cache.addMessage(message);
if (message.senderId != selectedFriend?.id) {
addUnread(message);
updateFriendStatus(message.senderId);
} else {
markMessagesRead(MarkReadBatch(senderId: message.senderId, ids: [message.id], readTime: DateTime.now()));
}
notifyListeners();
break;
case EventTarget.messagesRead:
final messageIds = args[0]["ids"] as List;
final recipientId = args[0]["recipientId"];
if (recipientId == null) break;
final cache = getUserMessageCache(recipientId);
if (cache == null) break;
for (var id in messageIds) {
cache.setMessageState(id, MessageState.read);
}
notifyListeners();
break;
}
} }
void _sendData(data) { Map<String, Session> createSessionMap(String salt) {
if (_wsChannel == null) throw "Neos Hub is not connected"; return _sessionMap.map((key, value) => MapEntry(CryptoHelper.idHash(value.id + salt), value));
_wsChannel!.add(jsonEncode(data)+_eofChar); }
void _onMessageSent(List args) {
final msg = args[0];
final message = Message.fromMap(msg, withState: MessageState.sent);
final cache = getUserMessageCache(message.recipientId) ?? _createUserMessageCache(message.recipientId);
cache.addMessage(message);
notifyListeners();
}
void _onReceiveMessage(List args) {
final msg = args[0];
final message = Message.fromMap(msg);
final cache = getUserMessageCache(message.senderId) ?? _createUserMessageCache(message.senderId);
cache.addMessage(message);
if (message.senderId != selectedFriend?.id) {
addUnread(message);
updateFriendStatus(message.senderId);
} else {
markMessagesRead(MarkReadBatch(senderId: message.senderId, ids: [message.id], readTime: DateTime.now()));
}
notifyListeners();
}
void _onMessagesRead(List args) {
final messageIds = args[0]["ids"] as List;
final recipientId = args[0]["recipientId"];
if (recipientId == null) return;
final cache = getUserMessageCache(recipientId);
if (cache == null) return;
for (var id in messageIds) {
cache.setMessageState(id, MessageState.read);
}
notifyListeners();
}
void _onReceiveStatusUpdate(List args) {
final statusUpdate = args[0];
var status = UserStatus.fromMap(statusUpdate);
final sessionMap = createSessionMap(status.hashSalt);
status = status.copyWith(
sessionData: status.sessions.map((e) => sessionMap[e.sessionHash] ?? Session.none()).toList());
final friend = getAsFriend(statusUpdate["userId"])?.copyWith(userStatus: status);
if (friend != null) {
_updateContact(friend);
}
notifyListeners();
}
void _onReceiveSessionUpdate(List args) {
final sessionUpdate = args[0];
final session = Session.fromMap(sessionUpdate);
_sessionMap[session.id] = session;
notifyListeners();
}
void _onRemoveSession(List args) {
final session = args[0];
_sessionMap.remove(session);
notifyListeners();
} }
} }

View file

@ -1,8 +1,8 @@
import 'dart:convert'; import 'dart:convert';
import 'package:contacts_plus_plus/auxiliary.dart'; import 'package:recon/auxiliary.dart';
import 'package:contacts_plus_plus/models/message.dart'; import 'package:recon/models/message.dart';
import 'package:contacts_plus_plus/models/session.dart'; import 'package:recon/models/session.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart' as fln; import 'package:flutter_local_notifications/flutter_local_notifications.dart' as fln;
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';

View file

@ -1,8 +1,8 @@
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:contacts_plus_plus/apis/session_api.dart'; import 'package:recon/apis/session_api.dart';
import 'package:contacts_plus_plus/clients/api_client.dart'; import 'package:recon/clients/api_client.dart';
import 'package:contacts_plus_plus/clients/settings_client.dart'; import 'package:recon/clients/settings_client.dart';
import 'package:contacts_plus_plus/models/session.dart'; import 'package:recon/models/session.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
class SessionClient extends ChangeNotifier { class SessionClient extends ChangeNotifier {

View file

@ -1,6 +1,6 @@
import 'dart:convert'; import 'dart:convert';
import 'package:contacts_plus_plus/models/settings.dart'; import 'package:recon/models/settings.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart';

View file

@ -1,13 +1,9 @@
class Config { class Config {
static const String apiBaseUrl = "https://api.neos.com"; static const String apiBaseUrl = "https://api.resonite.com";
static const String legacyCloudUrl = "https://neoscloud.blob.core.windows.net/assets/"; static const String skyfrostAssetsUrl = "https://assets.resonite.com";
static const String blobStorageUrl = "https://cloudxstorage.blob.core.windows.net/assets/"; static const String resoniteHubUrl = "$apiBaseUrl/hub";
static const String videoStorageUrl = "https://cloudx-video.azureedge.net/";
static const String neosCdnUrl = "https://cloudx.azureedge.net/assets/";
static const String neosAssetsUrl = "https://cloudxstorage.blob.core.windows.net/assets/";
static const String neosHubUrl = "$apiBaseUrl/hub";
static const int messageCacheValiditySeconds = 90; static const int messageCacheValiditySeconds = 90;
static const String latestCompatHash = "jnnkdwkBqGv5+jlf1u/k7A=="; static const String latestCompatHash = "YPDxN4N9fu7ZgV+Nr/AHQw==";
} }

14
lib/crypto_helper.dart Normal file
View 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();
}

135
lib/hub_manager.dart Normal file
View file

@ -0,0 +1,135 @@
import 'dart:convert';
import 'dart:io';
import 'package:collection/collection.dart';
import 'package:recon/config.dart';
import 'package:recon/models/hub_events.dart';
import 'package:logging/logging.dart';
import 'package:uuid/uuid.dart';
class HubManager {
static const String _eofChar = "";
static const String _negotiationPacket = "{\"protocol\":\"json\", \"version\":1}$_eofChar";
static const List<int> _reconnectTimeoutsSeconds = [0, 5, 10, 20, 60];
final Logger _logger = Logger("Hub");
final Map<String, dynamic> _headers = {};
final Map<EventTarget, dynamic Function(List arguments)> _handlers = {};
final Map<String, dynamic Function(Map result)> _responseHandlers = {};
WebSocket? _wsChannel;
bool _isConnecting = false;
int _attempts = 0;
void setHandler(EventTarget target, Function(List args) function) {
_handlers[target] = function;
}
void setHeaders(Map<String, dynamic> headers) {
_headers.addAll(headers);
}
void _onDisconnected(error) async {
_wsChannel = null;
_logger.warning("Hub connection died with error '$error', reconnecting...");
await start();
}
Future<void> start() async {
if (_isConnecting) {
return;
}
_isConnecting = true;
_wsChannel = await _tryConnect();
_isConnecting = false;
_logger.info("Connected to Resonite Hub.");
_wsChannel!.done.then((error) => _onDisconnected(error));
_wsChannel!.listen(_handleEvent, onDone: () => _onDisconnected("Connection closed."), onError: _onDisconnected);
_wsChannel!.add(_negotiationPacket);
}
Future<WebSocket> _tryConnect() async {
while (true) {
try {
final ws = await WebSocket.connect(Config.resoniteHubUrl.replaceFirst("https://", "wss://"), headers: _headers);
_attempts = 0;
return ws;
} catch (e) {
final timeout = _reconnectTimeoutsSeconds[_attempts.clamp(0, _reconnectTimeoutsSeconds.length - 1)];
_logger.severe(e);
_logger.severe("Retrying in $timeout seconds");
await Future.delayed(Duration(seconds: timeout));
_attempts++;
}
}
}
void _handleEvent(event) {
final bodies = event.toString().split(_eofChar);
final eventBodies = bodies.whereNot((element) => element.isEmpty).map((e) => jsonDecode(e));
for (final body in eventBodies) {
final int? rawType = body["type"];
if (rawType == null) {
_logger.warning("Received empty event, content was $event");
continue;
}
if (rawType > EventType.values.length) {
_logger.info("Unhandled event type $rawType: $body");
continue;
}
switch (EventType.values[rawType]) {
case EventType.streamItem:
case EventType.completion:
final handler = _responseHandlers[body["invocationId"]];
handler?.call(body["result"] ?? {});
_logger.info("Received completion event: $rawType: $body");
break;
case EventType.cancelInvocation:
case EventType.undefined:
_logger.info("Received unhandled event: $rawType: $body");
break;
case EventType.streamInvocation:
case EventType.invocation:
_logger.info("Received invocation-event.");
_handleInvocation(body);
break;
case EventType.ping:
_logger.info("Received keep-alive.");
break;
case EventType.close:
_logger.severe("Received close-event: ${body["error"]}");
// Should we trigger a manual reconnect here or just let the remote service close the connection?
break;
}
}
}
void _handleInvocation(body) async {
final target = EventTarget.parse(body["target"]);
final args = body["arguments"] ?? [];
final handler = _handlers[target];
if (handler == null) {
_logger.info("Unhandled event received");
return;
}
handler(args);
}
void send(String target, {List arguments = const [], Function(Map data)? responseHandler}) {
final invocationId = const Uuid().v4();
final data = {
"type": EventType.invocation.index,
"invocationId": invocationId,
"target": target,
"arguments": arguments,
};
if (responseHandler != null) {
_responseHandlers[invocationId] = responseHandler;
}
if (_wsChannel == null) throw "Resonite Hub is not connected";
_wsChannel!.add(jsonEncode(data) + _eofChar);
}
void dispose() {
_wsChannel?.close();
}
}

View file

@ -1,19 +1,20 @@
import 'dart:developer'; import 'dart:developer';
import 'package:contacts_plus_plus/apis/github_api.dart'; import 'package:recon/apis/github_api.dart';
import 'package:contacts_plus_plus/client_holder.dart'; import 'package:recon/client_holder.dart';
import 'package:contacts_plus_plus/clients/api_client.dart'; import 'package:recon/clients/api_client.dart';
import 'package:contacts_plus_plus/clients/inventory_client.dart'; import 'package:recon/clients/inventory_client.dart';
import 'package:contacts_plus_plus/clients/messaging_client.dart'; import 'package:recon/clients/messaging_client.dart';
import 'package:contacts_plus_plus/clients/session_client.dart'; import 'package:recon/clients/session_client.dart';
import 'package:contacts_plus_plus/clients/settings_client.dart'; import 'package:recon/clients/settings_client.dart';
import 'package:contacts_plus_plus/models/sem_ver.dart'; import 'package:recon/models/sem_ver.dart';
import 'package:contacts_plus_plus/widgets/homepage.dart'; import 'package:recon/widgets/homepage.dart';
import 'package:contacts_plus_plus/widgets/login_screen.dart'; import 'package:recon/widgets/login_screen.dart';
import 'package:contacts_plus_plus/widgets/update_notifier.dart'; import 'package:recon/widgets/update_notifier.dart';
import 'package:dynamic_color/dynamic_color.dart'; import 'package:dynamic_color/dynamic_color.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_downloader/flutter_downloader.dart'; import 'package:flutter_downloader/flutter_downloader.dart';
import 'package:flutter_phoenix/flutter_phoenix.dart'; import 'package:flutter_phoenix/flutter_phoenix.dart';
import 'package:hive_flutter/hive_flutter.dart'; import 'package:hive_flutter/hive_flutter.dart';
@ -47,20 +48,20 @@ void main() async {
cachedAuth = await ApiClient.tryCachedLogin(); cachedAuth = await ApiClient.tryCachedLogin();
} catch (_) {} } catch (_) {}
runApp(ContactsPlusPlus(settingsClient: settingsClient, cachedAuthentication: cachedAuth)); runApp(ReCon(settingsClient: settingsClient, cachedAuthentication: cachedAuth));
} }
class ContactsPlusPlus extends StatefulWidget { class ReCon extends StatefulWidget {
const ContactsPlusPlus({required this.settingsClient, required this.cachedAuthentication, super.key}); const ReCon({required this.settingsClient, required this.cachedAuthentication, super.key});
final SettingsClient settingsClient; final SettingsClient settingsClient;
final AuthenticationData cachedAuthentication; final AuthenticationData cachedAuthentication;
@override @override
State<ContactsPlusPlus> createState() => _ContactsPlusPlusState(); State<ReCon> createState() => _ReConState();
} }
class _ContactsPlusPlusState extends State<ContactsPlusPlus> { class _ReConState extends State<ReCon> {
final Typography _typography = Typography.material2021(platform: TargetPlatform.android); final Typography _typography = Typography.material2021(platform: TargetPlatform.android);
late AuthenticationData _authData = widget.cachedAuthentication; late AuthenticationData _authData = widget.cachedAuthentication;
bool _checkedForUpdate = false; bool _checkedForUpdate = false;
@ -128,7 +129,7 @@ class _ContactsPlusPlusState extends State<ContactsPlusPlus> {
child: DynamicColorBuilder( child: DynamicColorBuilder(
builder: (ColorScheme? lightDynamic, ColorScheme? darkDynamic) => MaterialApp( builder: (ColorScheme? lightDynamic, ColorScheme? darkDynamic) => MaterialApp(
debugShowCheckedModeBanner: false, debugShowCheckedModeBanner: false,
title: 'Contacts++', title: 'ReCon',
theme: ThemeData( theme: ThemeData(
useMaterial3: true, useMaterial3: true,
textTheme: _typography.black, textTheme: _typography.black,

View file

@ -1,35 +1,50 @@
class AuthenticationData { class AuthenticationData {
static const _unauthenticated = AuthenticationData(userId: "", token: "", secretMachineId: "", isAuthenticated: false); static const _unauthenticated = AuthenticationData(
userId: "",
token: "",
secretMachineIdHash: "",
isAuthenticated: false,
uid: "",
);
final String userId; final String userId;
final String token; final String token;
final String secretMachineId; final String secretMachineIdHash;
final bool isAuthenticated; final bool isAuthenticated;
final String uid;
const AuthenticationData({ const AuthenticationData({
required this.userId, required this.token, required this.secretMachineId, required this.isAuthenticated required this.userId,
required this.token,
required this.secretMachineIdHash,
required this.isAuthenticated,
required this.uid,
}); });
factory AuthenticationData.fromMap(Map map) { factory AuthenticationData.fromMap(Map map) {
map = map["entity"];
final userId = map["userId"]; final userId = map["userId"];
final token = map["token"]; final token = map["token"];
final machineId = map["secretMachineId"]; final machineId = map["secretMachineIdHash"];
if (userId == null || token == null || machineId == null) { final uid = map["uid"];
if (userId == null || token == null || machineId == null || uid == null) {
return _unauthenticated; return _unauthenticated;
} }
return AuthenticationData(userId: userId, token: token, secretMachineId: machineId, isAuthenticated: true); return AuthenticationData(userId: userId, token: token, secretMachineIdHash: machineId, isAuthenticated: true, uid: uid);
} }
factory AuthenticationData.unauthenticated() => _unauthenticated; factory AuthenticationData.unauthenticated() => _unauthenticated;
Map<String, String> get authorizationHeader => { Map<String, String> get authorizationHeader => {
"Authorization": "neos $userId:$token" "Authorization": "res $userId:$token",
}; "UID": uid,
};
Map<String, dynamic> toMap() { Map<String, dynamic> toMap() {
return { return {
"userId": userId, "userId": userId,
"token": token, "token": token,
"secretMachineId": secretMachineId, "secretMachineId": secretMachineIdHash,
"uid": uid,
}; };
} }
} }

View file

@ -1,4 +1,4 @@
import 'package:contacts_plus_plus/auxiliary.dart'; import 'package:recon/auxiliary.dart';
class CloudVariable { class CloudVariable {
final String ownerId; final String ownerId;

View file

@ -0,0 +1,28 @@
enum EventType {
undefined,
invocation,
streamItem,
completion,
streamInvocation,
cancelInvocation,
ping,
close;
}
enum EventTarget {
unknown,
messageSent,
receiveMessage,
messagesRead,
receiveSessionUpdate,
removeSession,
receiveStatusUpdate;
factory EventTarget.parse(String? text) {
if (text == null) return EventTarget.unknown;
return EventTarget.values.firstWhere(
(element) => element.name.toLowerCase() == text.toLowerCase(),
orElse: () => EventTarget.unknown,
);
}
}

View file

@ -1,64 +0,0 @@
import 'package:collection/collection.dart';
import 'package:contacts_plus_plus/stack.dart';
import 'package:contacts_plus_plus/models/records/record.dart';
class NeosPath {
static const _root = "Inventory";
final Stack<NeosDirectory> _pathStack = Stack<NeosDirectory>();
String get absolute {
if (_pathStack.isEmpty) return _root;
var path = _pathStack.entries.join("\\");
return "$_root\\$path";
}
NeosDirectory pop() => _pathStack.pop();
void push(NeosDirectory directory) => _pathStack.push(directory);
bool get isRoot => _pathStack.isEmpty;
/*
NeosDirectory get current => _pathStack.peek ?? NeosDirectory(name: _root);
void populateCurrent(String target, Iterable<Record> records) {
var currentDir = _pathStack.peek;
if (currentDir?.name != target) return;
currentDir?.records.addAll(records);
}
*/
}
class NeosDirectory {
static const rootName = "Inventory";
final Record record;
final NeosDirectory? parent;
final List<NeosDirectory> children;
NeosDirectory({required this.record, this.parent, required this.children});
factory NeosDirectory.fromRecord({required Record record, NeosDirectory? parent}) {
return NeosDirectory(record: record, parent: parent, children: []);
}
@override
String toString() {
return record.name;
}
bool get isRoot => record.isRoot;
String get absolutePath => "${parent?.absolutePath ?? ""}/${(record.name)}";
List<String> get absolutePathSegments => (parent?.absolutePathSegments ?? []) + [record.name];
bool containsRecord(Record record) => children.where((element) => element.record.id == record.id).isNotEmpty;
List<Record> get records => children.map((e) => e.record).toList();
bool get isLoaded => children.isNotEmpty;
NeosDirectory? findChildByRecord(Record record) => children.firstWhereOrNull((element) => element.record.id == record.id);
}

View file

@ -0,0 +1,35 @@
import 'package:collection/collection.dart';
import 'package:recon/models/records/record.dart';
class ResoniteDirectory {
static const rootName = "Inventory";
final Record record;
final ResoniteDirectory? parent;
final List<ResoniteDirectory> children;
ResoniteDirectory({required this.record, this.parent, required this.children});
factory ResoniteDirectory.fromRecord({required Record record, ResoniteDirectory? parent}) {
return ResoniteDirectory(record: record, parent: parent, children: []);
}
@override
String toString() {
return record.name;
}
bool get isRoot => record.isRoot;
String get absolutePath => "${parent?.absolutePath ?? ""}/${(record.name)}";
List<String> get absolutePathSegments => (parent?.absolutePathSegments ?? []) + [record.name];
bool containsRecord(Record record) => children.where((element) => element.record.id == record.id).isNotEmpty;
List<Record> get records => children.map((e) => e.record).toList();
bool get isLoaded => children.isNotEmpty;
ResoniteDirectory? findChildByRecord(Record record) => children.firstWhereOrNull((element) => element.record.id == record.id);
}

View file

@ -1,10 +1,10 @@
import 'dart:async'; import 'dart:async';
import 'dart:developer'; import 'dart:developer';
import 'package:contacts_plus_plus/clients/api_client.dart'; import 'package:recon/clients/api_client.dart';
import 'package:contacts_plus_plus/apis/message_api.dart'; import 'package:recon/apis/message_api.dart';
import 'package:contacts_plus_plus/auxiliary.dart'; import 'package:recon/auxiliary.dart';
import 'package:contacts_plus_plus/string_formatter.dart'; import 'package:recon/string_formatter.dart';
import 'package:uuid/uuid.dart'; import 'package:uuid/uuid.dart';
enum MessageType { enum MessageType {

View file

@ -1,4 +1,4 @@
import 'package:contacts_plus_plus/models/users/user_profile.dart'; import 'package:recon/models/users/user_profile.dart';
class PersonalProfile { class PersonalProfile {
final String id; final String id;

View file

@ -1,7 +1,7 @@
import 'package:contacts_plus_plus/models/records/neos_db_asset.dart'; import 'package:recon/models/records/resonite_db_asset.dart';
class AssetDiff extends NeosDBAsset{ class AssetDiff extends ResoniteDBAsset{
final Diff state; final Diff state;
final bool isUploaded; final bool isUploaded;

View file

@ -1,25 +1,25 @@
import 'dart:typed_data'; import 'dart:typed_data';
import 'package:contacts_plus_plus/models/records/neos_db_asset.dart'; import 'package:recon/models/records/resonite_db_asset.dart';
import 'package:path/path.dart'; import 'package:path/path.dart';
class AssetDigest { class AssetDigest {
final Uint8List data; final Uint8List data;
final NeosDBAsset asset; final ResoniteDBAsset asset;
final String name; final String name;
final String dbUri; final String dbUri;
AssetDigest({required this.data, required this.asset, required this.name, required this.dbUri}); AssetDigest({required this.data, required this.asset, required this.name, required this.dbUri});
static Future<AssetDigest> fromData(Uint8List data, String filename) async { static Future<AssetDigest> fromData(Uint8List data, String filename) async {
final asset = NeosDBAsset.fromData(data); final asset = ResoniteDBAsset.fromData(data);
return AssetDigest( return AssetDigest(
data: data, data: data,
asset: asset, asset: asset,
name: basenameWithoutExtension(filename), name: basenameWithoutExtension(filename),
dbUri: "neosdb:///${asset.hash}${extension(filename)}", dbUri: "resdb:///${asset.hash}${extension(filename)}",
); );
} }
} }

View file

@ -2,7 +2,7 @@ import 'package:path/path.dart';
import 'package:uuid/uuid.dart'; import 'package:uuid/uuid.dart';
class JsonTemplate { class JsonTemplate {
static const String thumbUrl = "neosdb:///8ed80703e48c3d1556093927b67298f3d5e10315e9f782ec56fc49d6366f09b7.webp"; static const String thumbUrl = "resdb:///8ed80703e48c3d1556093927b67298f3d5e10315e9f782ec56fc49d6366f09b7.webp";
final Map data; final Map data;
JsonTemplate({required this.data}); JsonTemplate({required this.data});
@ -2371,7 +2371,7 @@ class JsonTemplate {
}, },
"URL": { "URL": {
"ID": const Uuid().v4(), "ID": const Uuid().v4(),
"Data": "@neosdb:///3738bf6fc560f7d08d872ce12b06f4d9337ac5da415b6de6008a49ca128658ec" "Data": "@resdb:///3738bf6fc560f7d08d872ce12b06f4d9337ac5da415b6de6008a49ca128658ec"
}, },
"Readable": { "Readable": {
"ID": const Uuid().v4(), "ID": const Uuid().v4(),
@ -2444,7 +2444,7 @@ class JsonTemplate {
}, },
"URL": { "URL": {
"ID": const Uuid().v4(), "ID": const Uuid().v4(),
"Data": "@neosdb:///c801b8d2522fb554678f17f4597158b1af3f9be3abd6ce35d5a3112a81e2bf39" "Data": "@resdb:///c801b8d2522fb554678f17f4597158b1af3f9be3abd6ce35d5a3112a81e2bf39"
}, },
"Padding": { "Padding": {
"ID": const Uuid().v4(), "ID": const Uuid().v4(),
@ -2478,7 +2478,7 @@ class JsonTemplate {
}, },
"URL": { "URL": {
"ID": const Uuid().v4(), "ID": const Uuid().v4(),
"Data": "@neosdb:///4cac521169034ddd416c6deffe2eb16234863761837df677a910697ec5babd25" "Data": "@resdb:///4cac521169034ddd416c6deffe2eb16234863761837df677a910697ec5babd25"
}, },
"Padding": { "Padding": {
"ID": const Uuid().v4(), "ID": const Uuid().v4(),
@ -2512,7 +2512,7 @@ class JsonTemplate {
}, },
"URL": { "URL": {
"ID": const Uuid().v4(), "ID": const Uuid().v4(),
"Data": "@neosdb:///23e7ad7cb0a5a4cf75e07c9e0848b1eb06bba15e8fa9b8cb0579fc823c532927" "Data": "@resdb:///23e7ad7cb0a5a4cf75e07c9e0848b1eb06bba15e8fa9b8cb0579fc823c532927"
}, },
"Padding": { "Padding": {
"ID": const Uuid().v4(), "ID": const Uuid().v4(),
@ -2546,7 +2546,7 @@ class JsonTemplate {
}, },
"URL": { "URL": {
"ID": const Uuid().v4(), "ID": const Uuid().v4(),
"Data": "@neosdb:///415dc6290378574135b64c808dc640c1df7531973290c4970c51fdeb849cb0c5" "Data": "@resdb:///415dc6290378574135b64c808dc640c1df7531973290c4970c51fdeb849cb0c5"
}, },
"Padding": { "Padding": {
"ID": const Uuid().v4(), "ID": const Uuid().v4(),
@ -2580,7 +2580,7 @@ class JsonTemplate {
}, },
"URL": { "URL": {
"ID": const Uuid().v4(), "ID": const Uuid().v4(),
"Data": "@neosdb:///bcda0bcc22bab28ea4fedae800bfbf9ec76d71cc3b9f851779a35b7e438a839d" "Data": "@resdb:///bcda0bcc22bab28ea4fedae800bfbf9ec76d71cc3b9f851779a35b7e438a839d"
}, },
"Padding": { "Padding": {
"ID": const Uuid().v4(), "ID": const Uuid().v4(),
@ -2720,7 +2720,7 @@ class JsonTemplate {
}, },
"URL": { "URL": {
"ID": const Uuid().v4(), "ID": const Uuid().v4(),
"Data": "@neosdb:///274f0d4ea4bce93abc224c9ae9f9a97a9a396b382c5338f71c738d1591dd5c35.webp" "Data": "@resdb:///274f0d4ea4bce93abc224c9ae9f9a97a9a396b382c5338f71c738d1591dd5c35.webp"
}, },
"FilterMode": { "FilterMode": {
"ID": const Uuid().v4(), "ID": const Uuid().v4(),

View file

@ -1,26 +0,0 @@
import 'dart:typed_data';
import 'package:crypto/crypto.dart';
class NeosDBAsset {
final String hash;
final int bytes;
const NeosDBAsset({required this.hash, required this.bytes});
factory NeosDBAsset.fromMap(Map map) {
return NeosDBAsset(hash: map["hash"] ?? "", bytes: map["bytes"] ?? -1);
}
factory NeosDBAsset.fromData(Uint8List data) {
final digest = sha256.convert(data);
return NeosDBAsset(hash: digest.toString().replaceAll("-", "").toLowerCase(), bytes: data.length);
}
Map toMap() {
return {
"hash": hash,
"bytes": bytes,
};
}
}

View file

@ -1,4 +1,4 @@
import 'package:contacts_plus_plus/models/records/asset_diff.dart'; import 'package:recon/models/records/asset_diff.dart';
enum RecordPreprocessState enum RecordPreprocessState
{ {

View file

@ -1,10 +1,9 @@
import 'package:contacts_plus_plus/auxiliary.dart'; import 'package:recon/auxiliary.dart';
import 'package:contacts_plus_plus/models/message.dart'; import 'package:recon/models/message.dart';
import 'package:contacts_plus_plus/models/records/asset_digest.dart'; import 'package:recon/models/records/asset_digest.dart';
import 'package:contacts_plus_plus/models/records/neos_db_asset.dart'; import 'package:recon/models/records/resonite_db_asset.dart';
import 'package:contacts_plus_plus/string_formatter.dart'; import 'package:recon/string_formatter.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:uuid/uuid.dart'; import 'package:uuid/uuid.dart';
enum RecordType { enum RecordType {
@ -59,7 +58,7 @@ class Record {
isListed: false, isListed: false,
isForPatreons: false, isForPatreons: false,
lastModificationTime: DateTimeX.epoch, lastModificationTime: DateTimeX.epoch,
neosDBManifest: [], resoniteDBManifest: [],
lastModifyingUserId: "", lastModifyingUserId: "",
lastModifyingMachineId: "", lastModifyingMachineId: "",
creationTime: DateTimeX.epoch, creationTime: DateTimeX.epoch,
@ -100,7 +99,7 @@ class Record {
final int rating; final int rating;
final int randomOrder; final int randomOrder;
final List<String> manifest; final List<String> manifest;
final List<NeosDBAsset> neosDBManifest; final List<ResoniteDBAsset> resoniteDBManifest;
final String url; final String url;
final bool isValidOwnerId; final bool isValidOwnerId;
final bool isValidRecordId; final bool isValidRecordId;
@ -122,7 +121,7 @@ class Record {
required this.isListed, required this.isListed,
required this.isForPatreons, required this.isForPatreons,
required this.lastModificationTime, required this.lastModificationTime,
required this.neosDBManifest, required this.resoniteDBManifest,
required this.lastModifyingUserId, required this.lastModifyingUserId,
required this.lastModifyingMachineId, required this.lastModifyingMachineId,
required this.creationTime, required this.creationTime,
@ -153,14 +152,14 @@ class Record {
combinedRecordId: combinedRecordId, combinedRecordId: combinedRecordId,
assetUri: assetUri, assetUri: assetUri,
name: filename, name: filename,
tags: ([filename, "message_item", "message_id:${Message.generateId()}", "contacts-plus-plus"] + (extraTags ?? [])) tags: ([filename, "message_item", "message_id:${Message.generateId()}", "recon"] + (extraTags ?? []))
.unique(), .unique(),
recordType: recordType, recordType: recordType,
thumbnailUri: thumbnailUri, thumbnailUri: thumbnailUri,
isPublic: false, isPublic: false,
isForPatreons: false, isForPatreons: false,
isListed: false, isListed: false,
neosDBManifest: digests.map((e) => e.asset).toList(), resoniteDBManifest: digests.map((e) => e.asset).toList(),
globalVersion: 0, globalVersion: 0,
localVersion: 1, localVersion: 1,
lastModifyingUserId: userId, lastModifyingUserId: userId,
@ -173,7 +172,7 @@ class Record {
path: '', path: '',
description: '', description: '',
manifest: digests.map((e) => e.dbUri).toList(), manifest: digests.map((e) => e.dbUri).toList(),
url: "neosrec:///$userId/${combinedRecordId.id}", url: "resrec:///$userId/${combinedRecordId.id}",
isValidOwnerId: true, isValidOwnerId: true,
isValidRecordId: true, isValidRecordId: true,
visits: 0, visits: 0,
@ -199,14 +198,14 @@ class Record {
isForPatreons: map["isForPatreons"] ?? false, isForPatreons: map["isForPatreons"] ?? false,
isListed: map["isListed"] ?? false, isListed: map["isListed"] ?? false,
lastModificationTime: DateTime.tryParse(map["lastModificationTime"]) ?? DateTimeX.epoch, lastModificationTime: DateTime.tryParse(map["lastModificationTime"]) ?? DateTimeX.epoch,
neosDBManifest: (map["neosDBManifest"] as List? ?? []).map((e) => NeosDBAsset.fromMap(e)).toList(), resoniteDBManifest: (map["resoniteDBManifest"] as List? ?? []).map((e) => ResoniteDBAsset.fromMap(e)).toList(),
lastModifyingUserId: map["lastModifyingUserId"] ?? "", lastModifyingUserId: map["lastModifyingUserId"] ?? "",
lastModifyingMachineId: map["lastModifyingMachineId"] ?? "", lastModifyingMachineId: map["lastModifyingMachineId"] ?? "",
creationTime: DateTime.tryParse(map["lastModificationTime"]) ?? DateTimeX.epoch, creationTime: DateTime.tryParse(map["lastModificationTime"]) ?? DateTimeX.epoch,
isSynced: map["isSynced"] ?? false, isSynced: map["isSynced"] ?? false,
fetchedOn: DateTime.tryParse(map["fetchedOn"] ?? "") ?? DateTimeX.epoch, fetchedOn: DateTime.tryParse(map["fetchedOn"] ?? "") ?? DateTimeX.epoch,
path: map["path"] ?? "", path: map["path"] ?? "",
manifest: (map["neosDBManifest"] as List? ?? []).map((e) => e.toString()).toList(), manifest: (map["resoniteDBManifest"] as List? ?? []).map((e) => e.toString()).toList(),
url: map["url"] ?? "", url: map["url"] ?? "",
isValidOwnerId: map["isValidOwnerId"] == "true", isValidOwnerId: map["isValidOwnerId"] == "true",
isValidRecordId: map["isValidRecordId"] == "true", isValidRecordId: map["isValidRecordId"] == "true",
@ -220,7 +219,7 @@ class Record {
bool get isRoot => this == _rootRecord; bool get isRoot => this == _rootRecord;
String get linkRecordId { String get linkRecordId {
if (!assetUri.startsWith("neosrec")) { if (!assetUri.startsWith("resrec")) {
throw "Record is not a link."; throw "Record is not a link.";
} }
@ -233,11 +232,11 @@ class Record {
} }
String get linkOwnerId { String get linkOwnerId {
if (!assetUri.startsWith("neosrec")) { if (!assetUri.startsWith("resrec")) {
throw "Record is not a link."; throw "Record is not a link.";
} }
String ownerId = assetUri.replaceFirst("neosrec:///", ""); String ownerId = assetUri.replaceFirst("resrec:///", "");
final lastSlashIdx = ownerId.lastIndexOf("/"); final lastSlashIdx = ownerId.lastIndexOf("/");
if (lastSlashIdx == -1) { if (lastSlashIdx == -1) {
@ -265,7 +264,7 @@ class Record {
bool? isListed, bool? isListed,
bool? isDeleted, bool? isDeleted,
DateTime? lastModificationTime, DateTime? lastModificationTime,
List<NeosDBAsset>? neosDBManifest, List<ResoniteDBAsset>? resoniteDBManifest,
String? lastModifyingUserId, String? lastModifyingUserId,
String? lastModifyingMachineId, String? lastModifyingMachineId,
DateTime? creationTime, DateTime? creationTime,
@ -296,7 +295,7 @@ class Record {
isForPatreons: isForPatreons ?? this.isForPatreons, isForPatreons: isForPatreons ?? this.isForPatreons,
isListed: isListed ?? this.isListed, isListed: isListed ?? this.isListed,
lastModificationTime: lastModificationTime ?? this.lastModificationTime, lastModificationTime: lastModificationTime ?? this.lastModificationTime,
neosDBManifest: neosDBManifest ?? this.neosDBManifest, resoniteDBManifest: resoniteDBManifest ?? this.resoniteDBManifest,
lastModifyingUserId: lastModifyingUserId ?? this.lastModifyingUserId, lastModifyingUserId: lastModifyingUserId ?? this.lastModifyingUserId,
lastModifyingMachineId: lastModifyingMachineId ?? this.lastModifyingMachineId, lastModifyingMachineId: lastModifyingMachineId ?? this.lastModifyingMachineId,
creationTime: creationTime ?? this.creationTime, creationTime: creationTime ?? this.creationTime,
@ -330,7 +329,7 @@ class Record {
"isForPatreons": isForPatreons, "isForPatreons": isForPatreons,
"isListed": isListed, "isListed": isListed,
"lastModificationTime": lastModificationTime.toUtc().toIso8601String(), "lastModificationTime": lastModificationTime.toUtc().toIso8601String(),
"neosDBManifest": neosDBManifest.map((e) => e.toMap()).toList(), "resoniteDBManifest": resoniteDBManifest.map((e) => e.toMap()).toList(),
"lastModifyingUserId": lastModifyingUserId, "lastModifyingUserId": lastModifyingUserId,
"lastModifyingMachineId": lastModifyingMachineId, "lastModifyingMachineId": lastModifyingMachineId,
"creationTime": creationTime.toUtc().toIso8601String(), "creationTime": creationTime.toUtc().toIso8601String(),

View file

@ -0,0 +1,26 @@
import 'dart:typed_data';
import 'package:crypto/crypto.dart';
class ResoniteDBAsset {
final String hash;
final int bytes;
const ResoniteDBAsset({required this.hash, required this.bytes});
factory ResoniteDBAsset.fromMap(Map map) {
return ResoniteDBAsset(hash: map["hash"] ?? "", bytes: map["bytes"] ?? -1);
}
factory ResoniteDBAsset.fromData(Uint8List data) {
final digest = sha256.convert(data);
return ResoniteDBAsset(hash: digest.toString().replaceAll("-", "").toLowerCase(), bytes: data.length);
}
Map toMap() {
return {
"hash": hash,
"bytes": bytes,
};
}
}

View file

@ -1,12 +1,14 @@
import 'package:contacts_plus_plus/config.dart'; import 'dart:convert';
import 'package:contacts_plus_plus/string_formatter.dart';
import 'package:recon/string_formatter.dart';
import 'package:crypto/crypto.dart';
class Session { class Session {
final String id; final String id;
final String name; final String name;
final FormatNode formattedName; final FormatNode formattedName;
final List<SessionUser> sessionUsers; final List<SessionUser> sessionUsers;
final String thumbnail; final String thumbnailUrl;
final int maxUsers; final int maxUsers;
final bool hasEnded; final bool hasEnded;
final bool isValid; final bool isValid;
@ -22,7 +24,7 @@ class Session {
required this.id, required this.id,
required this.name, required this.name,
required this.sessionUsers, required this.sessionUsers,
required this.thumbnail, required this.thumbnailUrl,
required this.maxUsers, required this.maxUsers,
required this.hasEnded, required this.hasEnded,
required this.isValid, required this.isValid,
@ -40,7 +42,7 @@ class Session {
id: "", id: "",
name: "", name: "",
sessionUsers: const [], sessionUsers: const [],
thumbnail: "", thumbnailUrl: "",
maxUsers: 0, maxUsers: 0,
hasEnded: true, hasEnded: true,
isValid: false, isValid: false,
@ -60,7 +62,7 @@ class Session {
id: map["sessionId"], id: map["sessionId"],
name: map["name"], name: map["name"],
sessionUsers: (map["sessionUsers"] as List? ?? []).map((entry) => SessionUser.fromMap(entry)).toList(), sessionUsers: (map["sessionUsers"] as List? ?? []).map((entry) => SessionUser.fromMap(entry)).toList(),
thumbnail: map["thumbnail"] ?? "", thumbnailUrl: map["thumbnailUrl"] ?? "",
maxUsers: map["maxUsers"] ?? 0, maxUsers: map["maxUsers"] ?? 0,
hasEnded: map["hasEnded"] ?? false, hasEnded: map["hasEnded"] ?? false,
isValid: map["isValid"] ?? true, isValid: map["isValid"] ?? true,
@ -78,7 +80,7 @@ class Session {
"sessionId": id, "sessionId": id,
"name": name, "name": name,
"sessionUsers": shallow ? [] : sessionUsers.map((e) => e.toMap()).toList(), "sessionUsers": shallow ? [] : sessionUsers.map((e) => e.toMap()).toList(),
"thumbnail": thumbnail, "thumbnail": thumbnailUrl,
"maxUsers": maxUsers, "maxUsers": maxUsers,
"hasEnded": hasEnded, "hasEnded": hasEnded,
"isValid": isValid, "isValid": isValid,
@ -97,15 +99,17 @@ class Session {
enum SessionAccessLevel { enum SessionAccessLevel {
unknown, unknown,
private, private,
friends, contacts,
friendsOfFriends, contactsPlus,
registeredUsers,
anyone; anyone;
static const _readableNamesMap = { static const _readableNamesMap = {
SessionAccessLevel.unknown: "Unknown", SessionAccessLevel.unknown: "Unknown",
SessionAccessLevel.private: "Private", SessionAccessLevel.private: "Private",
SessionAccessLevel.friends: "Contacts", SessionAccessLevel.contacts: "Contacts",
SessionAccessLevel.friendsOfFriends: "Contacts+", SessionAccessLevel.contactsPlus: "Contacts+",
SessionAccessLevel.registeredUsers: "Registered users",
SessionAccessLevel.anyone: "Anyone", SessionAccessLevel.anyone: "Anyone",
}; };
@ -117,7 +121,7 @@ enum SessionAccessLevel {
} }
String toReadableString() { String toReadableString() {
return SessionAccessLevel._readableNamesMap[this] ?? "Unknown"; return SessionAccessLevel._readableNamesMap[this] ?? SessionAccessLevel.unknown.toReadableString();
} }
} }
@ -177,7 +181,6 @@ class SessionFilterSettings {
String buildRequestString() => "?includeEmptyHeadless=$includeEmptyHeadless" String buildRequestString() => "?includeEmptyHeadless=$includeEmptyHeadless"
"${"&includeEnded=$includeEnded"}" "${"&includeEnded=$includeEnded"}"
"${name.isNotEmpty ? "&name=$name" : ""}" "${name.isNotEmpty ? "&name=$name" : ""}"
"${!includeIncompatible ? "&compatibilityHash=${Uri.encodeComponent(Config.latestCompatHash)}" : ""}"
"${hostName.isNotEmpty ? "&hostName=$hostName" : ""}" "${hostName.isNotEmpty ? "&hostName=$hostName" : ""}"
"${minActiveUsers > 0 ? "&minActiveUsers=$minActiveUsers" : ""}"; "${minActiveUsers > 0 ? "&minActiveUsers=$minActiveUsers" : ""}";

View file

@ -0,0 +1,38 @@
import 'package:recon/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,
};
}
}

View file

@ -1,7 +1,7 @@
import 'dart:convert'; import 'dart:convert';
import 'package:contacts_plus_plus/models/sem_ver.dart'; import 'package:recon/models/sem_ver.dart';
import 'package:contacts_plus_plus/models/users/online_status.dart'; import 'package:recon/models/users/online_status.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:uuid/uuid.dart'; import 'package:uuid/uuid.dart';

View file

@ -1,36 +1,36 @@
import 'package:contacts_plus_plus/auxiliary.dart'; import 'package:recon/auxiliary.dart';
import 'package:contacts_plus_plus/models/users/user_profile.dart'; import 'package:recon/models/users/user_profile.dart';
import 'package:contacts_plus_plus/models/users/friend_status.dart'; import 'package:recon/models/users/friend_status.dart';
import 'package:contacts_plus_plus/models/users/online_status.dart'; import 'package:recon/models/users/online_status.dart';
import 'package:contacts_plus_plus/models/users/user_status.dart'; import 'package:recon/models/users/user_status.dart';
class Friend implements Comparable { class Friend implements Comparable {
static const _emptyId = "-1"; static const _emptyId = "-1";
static const _neosBotId = "U-Neos"; static const _resoniteBotId = "U-Resonite";
final String id; final String id;
final String username; final String username;
final String ownerId; final String ownerId;
final UserStatus userStatus; final UserStatus userStatus;
final UserProfile userProfile; final UserProfile userProfile;
final FriendStatus friendStatus; final FriendStatus contactStatus;
final DateTime latestMessageTime; final DateTime latestMessageTime;
const Friend({required this.id, required this.username, required this.ownerId, required this.userStatus, required this.userProfile, const Friend({required this.id, required this.username, required this.ownerId, required this.userStatus, required this.userProfile,
required this.friendStatus, required this.latestMessageTime, required this.contactStatus, required this.latestMessageTime,
}); });
bool get isHeadless => userStatus.activeSessions.any((session) => session.headlessHost == true && session.hostUserId == id); bool get isHeadless => userStatus.outputDevice == "Headless";
factory Friend.fromMap(Map map) { factory Friend.fromMap(Map map) {
final userStatus = UserStatus.fromMap(map["userStatus"]); var userStatus = map["userStatus"] == null ? UserStatus.empty() : UserStatus.fromMap(map["userStatus"]);
return Friend( return Friend(
id: map["id"], id: map["id"],
username: map["friendUsername"], username: map["contactUsername"],
ownerId: map["ownerId"] ?? map["id"], ownerId: map["ownerId"] ?? map["id"],
// Neos bot status is always offline but should be displayed as online // Neos bot status is always offline but should be displayed as online
userStatus: map["id"] == _neosBotId ? userStatus.copyWith(onlineStatus: OnlineStatus.online) : userStatus, userStatus: map["id"] == _resoniteBotId ? userStatus.copyWith(onlineStatus: OnlineStatus.online) : userStatus,
userProfile: UserProfile.fromMap(map["profile"] ?? {}), userProfile: UserProfile.fromMap(map["profile"] ?? {}),
friendStatus: FriendStatus.fromString(map["friendStatus"]), contactStatus: FriendStatus.fromString(map["contactStatus"]),
latestMessageTime: map["latestMessageTime"] == null latestMessageTime: map["latestMessageTime"] == null
? DateTime.fromMillisecondsSinceEpoch(0) : DateTime.parse(map["latestMessageTime"]), ? DateTime.fromMillisecondsSinceEpoch(0) : DateTime.parse(map["latestMessageTime"]),
); );
@ -48,7 +48,7 @@ class Friend implements Comparable {
ownerId: "", ownerId: "",
userStatus: UserStatus.empty(), userStatus: UserStatus.empty(),
userProfile: UserProfile.empty(), userProfile: UserProfile.empty(),
friendStatus: FriendStatus.none, contactStatus: FriendStatus.none,
latestMessageTime: DateTimeX.epoch latestMessageTime: DateTimeX.epoch
); );
} }
@ -57,14 +57,14 @@ class Friend implements Comparable {
Friend copyWith({ Friend copyWith({
String? id, String? username, String? ownerId, UserStatus? userStatus, UserProfile? userProfile, String? id, String? username, String? ownerId, UserStatus? userStatus, UserProfile? userProfile,
FriendStatus? friendStatus, DateTime? latestMessageTime}) { FriendStatus? contactStatus, DateTime? latestMessageTime}) {
return Friend( return Friend(
id: id ?? this.id, id: id ?? this.id,
username: username ?? this.username, username: username ?? this.username,
ownerId: ownerId ?? this.ownerId, ownerId: ownerId ?? this.ownerId,
userStatus: userStatus ?? this.userStatus, userStatus: userStatus ?? this.userStatus,
userProfile: userProfile ?? this.userProfile, userProfile: userProfile ?? this.userProfile,
friendStatus: friendStatus ?? this.friendStatus, contactStatus: contactStatus ?? this.contactStatus,
latestMessageTime: latestMessageTime ?? this.latestMessageTime, latestMessageTime: latestMessageTime ?? this.latestMessageTime,
); );
} }
@ -72,11 +72,11 @@ class Friend implements Comparable {
Map toMap({bool shallow=false}) { Map toMap({bool shallow=false}) {
return { return {
"id": id, "id": id,
"username": username, "contactUsername": username,
"ownerId": ownerId, "ownerId": ownerId,
"userStatus": userStatus.toMap(shallow: shallow), "userStatus": userStatus.toMap(shallow: shallow),
"profile": userProfile.toMap(), "profile": userProfile.toMap(),
"friendStatus": friendStatus.name, "contactStatus": contactStatus.name,
"latestMessageTime": latestMessageTime.toIso8601String(), "latestMessageTime": latestMessageTime.toIso8601String(),
}; };
} }

View file

@ -19,7 +19,7 @@ enum OnlineStatus {
factory OnlineStatus.fromString(String? text) { factory OnlineStatus.fromString(String? text) {
return OnlineStatus.values.firstWhere((element) => element.name.toLowerCase() == text?.toLowerCase(), return OnlineStatus.values.firstWhere((element) => element.name.toLowerCase() == text?.toLowerCase(),
orElse: () => OnlineStatus.offline, orElse: () => OnlineStatus.online,
); );
} }

View file

@ -1,4 +1,4 @@
import 'package:contacts_plus_plus/models/users/user_profile.dart'; import 'package:recon/models/users/user_profile.dart';
class User { class User {
final String id; final String id;

View file

@ -1,56 +1,100 @@
import 'package:contacts_plus_plus/models/session.dart'; import 'package:recon/crypto_helper.dart';
import 'package:contacts_plus_plus/models/users/online_status.dart'; import 'package:recon/models/session.dart';
import 'package:recon/models/session_metadata.dart';
import 'package:recon/models/users/online_status.dart';
import 'package:uuid/uuid.dart';
enum UserSessionType
{
unknown,
graphicalClient,
chatClient,
headless,
not;
factory UserSessionType.fromString(String? text) {
return UserSessionType.values.firstWhere((element) => element.name.toLowerCase() == text?.toLowerCase(),
orElse: () => UserSessionType.unknown,
);
}
}
class UserStatus { class UserStatus {
final OnlineStatus onlineStatus; final OnlineStatus onlineStatus;
final DateTime lastStatusChange; final DateTime lastStatusChange;
final int currentSessionAccessLevel; final DateTime lastPresenceTimestamp;
final bool currentSessionHidden; final String userSessionId;
final bool currentHosting; final int currentSessionIndex;
final Session currentSession; final List<SessionMetadata> sessions;
final List<Session> activeSessions; final String appVersion;
final String neosVersion;
final String outputDevice; final String outputDevice;
final bool isMobile; final bool isMobile;
final bool isPresent;
final String compatibilityHash; final String compatibilityHash;
final String hashSalt;
final UserSessionType sessionType;
final List<Session> decodedSessions;
const UserStatus( const UserStatus({
{required this.onlineStatus, required this.lastStatusChange, required this.currentSession, required this.onlineStatus,
required this.currentSessionAccessLevel, required this.currentSessionHidden, required this.currentHosting, required this.lastStatusChange,
required this.activeSessions, required this.neosVersion, required this.outputDevice, required this.isMobile, required this.lastPresenceTimestamp,
required this.compatibilityHash, required this.userSessionId,
}); required this.currentSessionIndex,
required this.sessions,
required this.appVersion,
required this.outputDevice,
required this.isMobile,
required this.isPresent,
required this.compatibilityHash,
required this.hashSalt,
required this.sessionType,
this.decodedSessions = const []
});
factory UserStatus.initial() =>
UserStatus.empty().copyWith(
onlineStatus: OnlineStatus.online,
hashSalt: CryptoHelper.cryptoToken(),
outputDevice: "Mobile",
userSessionId: const Uuid().v4().toString(),
sessionType: UserSessionType.chatClient,
);
factory UserStatus.empty() => factory UserStatus.empty() =>
UserStatus( UserStatus(
onlineStatus: OnlineStatus.offline, onlineStatus: OnlineStatus.offline,
lastStatusChange: DateTime.now(), lastStatusChange: DateTime.now(),
currentSessionAccessLevel: 0, lastPresenceTimestamp: DateTime.now(),
currentSessionHidden: false, userSessionId: "",
currentHosting: false, currentSessionIndex: -1,
currentSession: Session.none(), sessions: [],
activeSessions: [], appVersion: "",
neosVersion: "",
outputDevice: "Unknown", outputDevice: "Unknown",
isMobile: false, isMobile: false,
isPresent: false,
compatibilityHash: "", compatibilityHash: "",
hashSalt: "",
sessionType: UserSessionType.unknown
); );
factory UserStatus.fromMap(Map map) { factory UserStatus.fromMap(Map map) {
final statusString = map["onlineStatus"] as String?; final statusString = map["onlineStatus"].toString();
final status = OnlineStatus.fromString(statusString); final status = OnlineStatus.fromString(statusString);
return UserStatus( return UserStatus(
onlineStatus: status, onlineStatus: status,
lastStatusChange: DateTime.parse(map["lastStatusChange"]), lastStatusChange: DateTime.tryParse(map["lastStatusChange"] ?? "") ?? DateTime.now(),
currentSessionAccessLevel: map["currentSessionAccessLevel"] ?? 0, lastPresenceTimestamp: DateTime.tryParse(map["lastPresenceTimestamp"] ?? "") ?? DateTime.now(),
currentSessionHidden: map["currentSessionHidden"] ?? false, userSessionId: map["userSessionId"] ?? "",
currentHosting: map["currentHosting"] ?? false, isPresent: map["isPresent"] ?? false,
currentSession: Session.fromMap(map["currentSession"]), currentSessionIndex: map["currentSessionIndex"] ?? -1,
activeSessions: (map["activeSessions"] as List? ?? []).map((e) => Session.fromMap(e)).toList(), sessions: (map["sessions"] as List? ?? []).map((e) => SessionMetadata.fromMap(e)).toList(),
neosVersion: map["neosVersion"] ?? "", appVersion: map["appVersion"] ?? "",
outputDevice: map["outputDevice"] ?? "Unknown", outputDevice: map["outputDevice"] ?? "Unknown",
isMobile: map["isMobile"] ?? false, isMobile: map["isMobile"] ?? false,
compatibilityHash: map["compatabilityHash"] ?? "" compatibilityHash: map["compatabilityHash"] ?? "",
hashSalt: map["hashSalt"] ?? "",
sessionType: UserSessionType.fromString(map["sessionType"])
); );
} }
@ -58,12 +102,18 @@ class UserStatus {
return { return {
"onlineStatus": onlineStatus.index, "onlineStatus": onlineStatus.index,
"lastStatusChange": lastStatusChange.toIso8601String(), "lastStatusChange": lastStatusChange.toIso8601String(),
"currentSessionAccessLevel": currentSessionAccessLevel, "isPresent": isPresent,
"currentSessionHidden": currentSessionHidden, "lastPresenceTimestamp": lastPresenceTimestamp.toIso8601String(),
"currentHosting": currentHosting, "userSessionId": userSessionId,
"currentSession": currentSession.isNone || shallow ? null : currentSession.toMap(), "currentSessionIndex": currentSessionIndex,
"activeSessions": shallow ? [] : activeSessions.map((e) => e.toMap(),).toList(), "sessions": shallow
"neosVersion": neosVersion, ? []
: sessions
.map(
(e) => e.toMap(),
)
.toList(),
"appVersion": appVersion,
"outputDevice": outputDevice, "outputDevice": outputDevice,
"isMobile": isMobile, "isMobile": isMobile,
"compatibilityHash": compatibilityHash, "compatibilityHash": compatibilityHash,
@ -73,27 +123,33 @@ class UserStatus {
UserStatus copyWith({ UserStatus copyWith({
OnlineStatus? onlineStatus, OnlineStatus? onlineStatus,
DateTime? lastStatusChange, DateTime? lastStatusChange,
int? currentSessionAccessLevel, DateTime? lastPresenceTimestamp,
bool? currentSessionHidden, bool? isPresent,
bool? currentHosting, String? userSessionId,
Session? currentSession, int? currentSessionIndex,
List<Session>? activeSessions, List<SessionMetadata>? sessions,
String? neosVersion, String? appVersion,
String? outputDevice, String? outputDevice,
bool? isMobile, bool? isMobile,
String? compatibilityHash, String? compatibilityHash,
String? hashSalt,
UserSessionType? sessionType,
List<Session>? sessionData,
}) => }) =>
UserStatus( UserStatus(
onlineStatus: onlineStatus ?? this.onlineStatus, onlineStatus: onlineStatus ?? this.onlineStatus,
lastStatusChange: lastStatusChange ?? this.lastStatusChange, lastStatusChange: lastStatusChange ?? this.lastStatusChange,
currentSessionAccessLevel: currentSessionAccessLevel ?? this.currentSessionAccessLevel, lastPresenceTimestamp: lastPresenceTimestamp ?? this.lastPresenceTimestamp,
currentSessionHidden: currentSessionHidden ?? this.currentSessionHidden, isPresent: isPresent ?? this.isPresent,
currentHosting: currentHosting ?? this.currentHosting, userSessionId: userSessionId ?? this.userSessionId,
currentSession: currentSession ?? this.currentSession, currentSessionIndex: currentSessionIndex ?? this.currentSessionIndex,
activeSessions: activeSessions ?? this.activeSessions, sessions: sessions ?? this.sessions,
neosVersion: neosVersion ?? this.neosVersion, appVersion: appVersion ?? this.appVersion,
outputDevice: outputDevice ?? this.outputDevice, outputDevice: outputDevice ?? this.outputDevice,
isMobile: isMobile ?? this.isMobile, isMobile: isMobile ?? this.isMobile,
compatibilityHash: compatibilityHash ?? this.compatibilityHash, compatibilityHash: compatibilityHash ?? this.compatibilityHash,
hashSalt: hashSalt ?? this.hashSalt,
sessionType: sessionType ?? this.sessionType,
decodedSessions: sessionData ?? this.decodedSessions,
); );
} }

View file

@ -1,4 +1,4 @@
import 'package:contacts_plus_plus/string_formatter.dart'; import 'package:recon/string_formatter.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
class FormattedText extends StatelessWidget { class FormattedText extends StatelessWidget {

View file

@ -1,11 +1,11 @@
import 'package:contacts_plus_plus/auxiliary.dart'; import 'package:recon/auxiliary.dart';
import 'package:contacts_plus_plus/clients/messaging_client.dart'; import 'package:recon/clients/messaging_client.dart';
import 'package:contacts_plus_plus/models/users/friend.dart'; import 'package:recon/models/message.dart';
import 'package:contacts_plus_plus/models/message.dart'; import 'package:recon/models/users/friend.dart';
import 'package:contacts_plus_plus/widgets/formatted_text.dart'; import 'package:recon/widgets/formatted_text.dart';
import 'package:contacts_plus_plus/widgets/friends/friend_online_status_indicator.dart'; import 'package:recon/widgets/friends/friend_online_status_indicator.dart';
import 'package:contacts_plus_plus/widgets/generic_avatar.dart'; import 'package:recon/widgets/generic_avatar.dart';
import 'package:contacts_plus_plus/widgets/messages/messages_list.dart'; import 'package:recon/widgets/messages/messages_list.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
@ -19,8 +19,12 @@ class FriendListTile extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final imageUri = Aux.neosDbToHttp(friend.userProfile.iconUrl); final imageUri = Aux.resdbToHttp(friend.userProfile.iconUrl);
final theme = Theme.of(context); final theme = Theme.of(context);
final mClient = Provider.of<MessagingClient>(context, listen: false);
final currentSession = friend.userStatus.currentSessionIndex == -1
? null
: friend.userStatus.decodedSessions.elementAtOrNull(friend.userStatus.currentSessionIndex);
return ListTile( return ListTile(
leading: GenericAvatar( leading: GenericAvatar(
imageUri: imageUri, imageUri: imageUri,
@ -54,11 +58,11 @@ class FriendListTile extends StatelessWidget {
width: 4, width: 4,
), ),
Text(toBeginningOfSentenceCase(friend.userStatus.onlineStatus.name) ?? "Unknown"), Text(toBeginningOfSentenceCase(friend.userStatus.onlineStatus.name) ?? "Unknown"),
if (!friend.userStatus.currentSession.isNone) ...[ if (currentSession != null && !currentSession.isNone) ...[
const Text(" in "), const Text(" in "),
Expanded( Expanded(
child: FormattedText( child: FormattedText(
friend.userStatus.currentSession.formattedName, currentSession.formattedName,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
maxLines: 1, maxLines: 1,
)) ))
@ -67,7 +71,6 @@ class FriendListTile extends StatelessWidget {
), ),
onTap: () async { onTap: () async {
onTap?.call(); onTap?.call();
final mClient = Provider.of<MessagingClient>(context, listen: false);
mClient.loadUserMessageCache(friend.id); mClient.loadUserMessageCache(friend.id);
final unreads = mClient.getUnreadsForFriend(friend); final unreads = mClient.getUnreadsForFriend(friend);
if (unreads.isNotEmpty) { if (unreads.isNotEmpty) {

View file

@ -1,5 +1,5 @@
import 'package:contacts_plus_plus/models/users/online_status.dart'; import 'package:recon/models/users/online_status.dart';
import 'package:contacts_plus_plus/models/users/user_status.dart'; import 'package:recon/models/users/user_status.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
class FriendOnlineStatusIndicator extends StatelessWidget { class FriendOnlineStatusIndicator extends StatelessWidget {
@ -9,7 +9,7 @@ class FriendOnlineStatusIndicator extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return userStatus.neosVersion.contains("Contacts++") && userStatus.onlineStatus != OnlineStatus.offline return userStatus.appVersion.contains("ReCon") && userStatus.onlineStatus != OnlineStatus.offline
? SizedBox.square( ? SizedBox.square(
dimension: 10, dimension: 10,
child: Image.asset( child: Image.asset(

View file

@ -1,7 +1,7 @@
import 'package:contacts_plus_plus/clients/messaging_client.dart'; import 'package:recon/clients/messaging_client.dart';
import 'package:contacts_plus_plus/widgets/default_error_widget.dart'; import 'package:recon/widgets/default_error_widget.dart';
import 'package:contacts_plus_plus/widgets/friends/expanding_input_fab.dart'; import 'package:recon/widgets/friends/expanding_input_fab.dart';
import 'package:contacts_plus_plus/widgets/friends/friend_list_tile.dart'; import 'package:recon/widgets/friends/friend_list_tile.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';

View file

@ -1,11 +1,10 @@
import 'package:contacts_plus_plus/apis/user_api.dart'; import 'package:recon/client_holder.dart';
import 'package:contacts_plus_plus/client_holder.dart'; import 'package:recon/clients/messaging_client.dart';
import 'package:contacts_plus_plus/clients/messaging_client.dart'; import 'package:recon/models/users/online_status.dart';
import 'package:contacts_plus_plus/models/users/online_status.dart'; import 'package:recon/widgets/friends/user_search.dart';
import 'package:contacts_plus_plus/models/users/user_status.dart'; import 'package:recon/widgets/my_profile_dialog.dart';
import 'package:contacts_plus_plus/widgets/friends/user_search.dart';
import 'package:contacts_plus_plus/widgets/my_profile_dialog.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
@ -17,133 +16,70 @@ class FriendsListAppBar extends StatefulWidget {
} }
class _FriendsListAppBarState extends State<FriendsListAppBar> with AutomaticKeepAliveClientMixin { class _FriendsListAppBarState extends State<FriendsListAppBar> with AutomaticKeepAliveClientMixin {
Future<UserStatus>? _userStatusFuture;
ClientHolder? _clientHolder;
@override
void didChangeDependencies() async {
super.didChangeDependencies();
final clientHolder = ClientHolder.of(context);
if (_clientHolder != clientHolder) {
_clientHolder = clientHolder;
_refreshUserStatus();
}
}
void _refreshUserStatus() {
final apiClient = _clientHolder!.apiClient;
_userStatusFuture ??= UserApi.getUserStatus(apiClient, userId: apiClient.userId).then((value) async {
if (value.onlineStatus == OnlineStatus.offline) {
final newStatus = value.copyWith(
onlineStatus:
OnlineStatus.values[_clientHolder!.settingsClient.currentSettings.lastOnlineStatus.valueOrDefault]);
await UserApi.setStatus(apiClient, status: newStatus);
return newStatus;
}
return value;
});
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
super.build(context); super.build(context);
return AppBar( return AppBar(
title: const Text("Contacts++"), title: const Text("ReCon"),
systemOverlayStyle: SystemUiOverlayStyle(
systemNavigationBarColor: Theme.of(context).navigationBarTheme.backgroundColor,
),
actions: [ actions: [
FutureBuilder( Consumer<MessagingClient>(builder: (context, client, _) {
future: _userStatusFuture, return PopupMenuButton<OnlineStatus>(
builder: (context, snapshot) { child: Row(
if (snapshot.hasData) { children: [
final userStatus = snapshot.data as UserStatus; Padding(
return PopupMenuButton<OnlineStatus>( padding: const EdgeInsets.only(right: 8.0),
child: Row( child: Icon(
children: [ Icons.circle,
Padding( size: 16,
padding: const EdgeInsets.only(right: 8.0), color: client.userStatus.onlineStatus.color(context),
child: Icon( ),
),
Text(toBeginningOfSentenceCase(client.userStatus.onlineStatus.name) ?? "Unknown"),
],
),
onSelected: (OnlineStatus onlineStatus) async {
final newStatus = client.userStatus.copyWith(onlineStatus: onlineStatus);
final settingsClient = ClientHolder.of(context).settingsClient;
try {
await client.setUserStatus(newStatus);
await settingsClient
.changeSettings(settingsClient.currentSettings.copyWith(lastOnlineStatus: onlineStatus.index));
} catch (e, s) {
FlutterError.reportError(FlutterErrorDetails(exception: e, stack: s));
if (context.mounted) {
ScaffoldMessenger.of(context)
.showSnackBar(const SnackBar(content: Text("Failed to set online-status.")));
}
}
},
itemBuilder: (BuildContext context) => OnlineStatus.values
.where((element) => element == OnlineStatus.online || element == OnlineStatus.invisible)
.map(
(item) => PopupMenuItem<OnlineStatus>(
value: item,
child: Row(
mainAxisAlignment: MainAxisAlignment.start,
children: [
Icon(
Icons.circle, Icons.circle,
size: 16, size: 16,
color: userStatus.onlineStatus.color(context), color: item.color(context),
), ),
), const SizedBox(
Text(toBeginningOfSentenceCase(userStatus.onlineStatus.name) ?? "Unknown"), width: 8,
], ),
), Text(toBeginningOfSentenceCase(item.name)!),
onSelected: (OnlineStatus onlineStatus) async { ],
try {
final newStatus = userStatus.copyWith(onlineStatus: onlineStatus);
setState(() {
_userStatusFuture = Future.value(newStatus.copyWith(lastStatusChange: DateTime.now()));
});
final settingsClient = _clientHolder!.settingsClient;
await UserApi.setStatus(_clientHolder!.apiClient, status: newStatus);
await settingsClient.changeSettings(
settingsClient.currentSettings.copyWith(lastOnlineStatus: onlineStatus.index));
} catch (e, s) {
FlutterError.reportError(FlutterErrorDetails(exception: e, stack: s));
ScaffoldMessenger.of(context)
.showSnackBar(const SnackBar(content: Text("Failed to set online-status.")));
setState(() {
_userStatusFuture = Future.value(userStatus);
});
}
},
itemBuilder: (BuildContext context) => OnlineStatus.values
.where((element) => element == OnlineStatus.online || element == OnlineStatus.invisible)
.map(
(item) => PopupMenuItem<OnlineStatus>(
value: item,
child: Row(
mainAxisAlignment: MainAxisAlignment.start,
children: [
Icon(
Icons.circle,
size: 16,
color: item.color(context),
),
const SizedBox(
width: 8,
),
Text(toBeginningOfSentenceCase(item.name)!),
],
),
), ),
)
.toList());
} else if (snapshot.hasError) {
return TextButton.icon(
style: TextButton.styleFrom(
foregroundColor: Theme.of(context).colorScheme.onSurface,
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 2)),
onPressed: () {
setState(() {
_userStatusFuture =
UserApi.getUserStatus(_clientHolder!.apiClient, userId: _clientHolder!.apiClient.userId);
});
},
icon: const Icon(Icons.warning),
label: const Text("Retry"),
);
} else {
return TextButton.icon(
style: TextButton.styleFrom(
disabledForegroundColor: Theme.of(context).colorScheme.onSurface,
),
onPressed: null,
icon: Container(
width: 16,
height: 16,
margin: const EdgeInsets.only(right: 4),
child: CircularProgressIndicator(
strokeWidth: 2,
color: Theme.of(context).colorScheme.onSurface,
), ),
), )
label: const Text("Loading"), .toList(),
); );
} }),
},
),
Padding( Padding(
padding: const EdgeInsets.only(left: 4, right: 4), padding: const EdgeInsets.only(left: 4, right: 4),
child: PopupMenuButton<MenuItemDefinition>( child: PopupMenuButton<MenuItemDefinition>(
@ -182,16 +118,16 @@ class _FriendsListAppBarState extends State<FriendsListAppBar> with AutomaticKee
] ]
.map( .map(
(item) => PopupMenuItem<MenuItemDefinition>( (item) => PopupMenuItem<MenuItemDefinition>(
value: item, value: item,
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
Text(item.name), Text(item.name),
Icon(item.icon), Icon(item.icon),
], ],
), ),
), ),
) )
.toList(), .toList(),
), ),
) )

View file

@ -1,8 +1,8 @@
import 'package:contacts_plus_plus/apis/user_api.dart'; import 'package:recon/apis/contact_api.dart';
import 'package:contacts_plus_plus/auxiliary.dart'; import 'package:recon/auxiliary.dart';
import 'package:contacts_plus_plus/client_holder.dart'; import 'package:recon/client_holder.dart';
import 'package:contacts_plus_plus/models/users/user.dart'; import 'package:recon/models/users/user.dart';
import 'package:contacts_plus_plus/widgets/generic_avatar.dart'; import 'package:recon/widgets/generic_avatar.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
@ -41,7 +41,7 @@ class _UserListTileState extends State<UserListTile> {
), ),
); );
return ListTile( return ListTile(
leading: GenericAvatar(imageUri: Aux.neosDbToHttp(widget.user.userProfile?.iconUrl),), leading: GenericAvatar(imageUri: Aux.resdbToHttp(widget.user.userProfile?.iconUrl),),
title: Text(widget.user.username), title: Text(widget.user.username),
subtitle: Text(_regDateFormat.format(widget.user.registrationDate)), subtitle: Text(_regDateFormat.format(widget.user.registrationDate)),
trailing: IconButton( trailing: IconButton(
@ -55,11 +55,11 @@ class _UserListTileState extends State<UserListTile> {
}); });
try { try {
if (_localAdded) { if (_localAdded) {
await UserApi.removeUserAsFriend(ClientHolder await ContactApi.removeUserAsFriend(ClientHolder
.of(context) .of(context)
.apiClient, user: widget.user); .apiClient, user: widget.user);
} else { } else {
await UserApi.addUserAsFriend(ClientHolder await ContactApi.addUserAsFriend(ClientHolder
.of(context) .of(context)
.apiClient, user: widget.user); .apiClient, user: widget.user);
} }
@ -70,7 +70,8 @@ class _UserListTileState extends State<UserListTile> {
widget.onChanged?.call(); widget.onChanged?.call();
} catch (e, s) { } catch (e, s) {
FlutterError.reportError(FlutterErrorDetails(exception: e, stack: s)); FlutterError.reportError(FlutterErrorDetails(exception: e, stack: s));
ScaffoldMessenger.of(context).showSnackBar( if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar( SnackBar(
duration: const Duration(seconds: 5), duration: const Duration(seconds: 5),
content: Text( content: Text(
@ -80,6 +81,7 @@ class _UserListTileState extends State<UserListTile> {
), ),
), ),
); );
}
setState(() { setState(() {
_loading = false; _loading = false;
}); });

View file

@ -1,11 +1,11 @@
import 'dart:async'; import 'dart:async';
import 'package:contacts_plus_plus/apis/user_api.dart'; import 'package:recon/apis/user_api.dart';
import 'package:contacts_plus_plus/client_holder.dart'; import 'package:recon/client_holder.dart';
import 'package:contacts_plus_plus/clients/messaging_client.dart'; import 'package:recon/clients/messaging_client.dart';
import 'package:contacts_plus_plus/models/users/user.dart'; import 'package:recon/models/users/user.dart';
import 'package:contacts_plus_plus/widgets/default_error_widget.dart'; import 'package:recon/widgets/default_error_widget.dart';
import 'package:contacts_plus_plus/widgets/friends/user_list_tile.dart'; import 'package:recon/widgets/friends/user_list_tile.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';

View file

@ -1,11 +1,11 @@
import 'package:contacts_plus_plus/widgets/friends/friends_list.dart'; import 'package:recon/widgets/friends/friends_list.dart';
import 'package:contacts_plus_plus/widgets/friends/friends_list_app_bar.dart'; import 'package:recon/widgets/friends/friends_list_app_bar.dart';
import 'package:contacts_plus_plus/widgets/inventory/inventory_browser.dart'; import 'package:recon/widgets/inventory/inventory_browser.dart';
import 'package:contacts_plus_plus/widgets/inventory/inventory_browser_app_bar.dart'; import 'package:recon/widgets/inventory/inventory_browser_app_bar.dart';
import 'package:contacts_plus_plus/widgets/sessions/session_list.dart'; import 'package:recon/widgets/sessions/session_list.dart';
import 'package:contacts_plus_plus/widgets/sessions/session_list_app_bar.dart'; import 'package:recon/widgets/sessions/session_list_app_bar.dart';
import 'package:contacts_plus_plus/widgets/settings_app_bar.dart'; import 'package:recon/widgets/settings_app_bar.dart';
import 'package:contacts_plus_plus/widgets/settings_page.dart'; import 'package:recon/widgets/settings_page.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
class Home extends StatefulWidget { class Home extends StatefulWidget {
@ -29,6 +29,7 @@ class _HomeState extends State<Home> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
backgroundColor: Theme.of(context).colorScheme.surface,
appBar: PreferredSize( appBar: PreferredSize(
preferredSize: const Size.fromHeight(kToolbarHeight), preferredSize: const Size.fromHeight(kToolbarHeight),
child: AnimatedSwitcher( child: AnimatedSwitcher(
@ -46,46 +47,36 @@ class _HomeState extends State<Home> {
SettingsPage(), SettingsPage(),
], ],
), ),
bottomNavigationBar: Container( bottomNavigationBar: NavigationBar(
decoration: BoxDecoration( selectedIndex: _selectedPage,
border: const Border(top: BorderSide(width: 1, color: Colors.black)), onDestinationSelected: (index) {
color: Theme.of(context).colorScheme.background, _pageController.animateToPage(
), index,
child: BottomNavigationBar( duration: const Duration(milliseconds: 200),
backgroundColor: Theme.of(context).colorScheme.surface, curve: Curves.easeOut,
type: BottomNavigationBarType.fixed, );
unselectedItemColor: Theme.of(context).colorScheme.onBackground, setState(() {
selectedItemColor: Theme.of(context).colorScheme.primary, _selectedPage = index;
currentIndex: _selectedPage, });
onTap: (index) { },
_pageController.animateToPage( destinations: const [
index, NavigationDestination(
duration: const Duration(milliseconds: 200), icon: Icon(Icons.message),
curve: Curves.easeOut, label: "Chat",
); ),
setState(() { NavigationDestination(
_selectedPage = index; icon: Icon(Icons.public),
}); label: "Sessions",
}, ),
items: const [ NavigationDestination(
BottomNavigationBarItem( icon: Icon(Icons.inventory),
icon: Icon(Icons.message), label: "Inventory",
label: "Chat", ),
), NavigationDestination(
BottomNavigationBarItem( icon: Icon(Icons.settings),
icon: Icon(Icons.public), label: "Settings",
label: "Sessions", ),
), ],
BottomNavigationBarItem(
icon: Icon(Icons.inventory),
label: "Inventory",
),
BottomNavigationBarItem(
icon: Icon(Icons.settings),
label: "Settings",
),
],
),
), ),
); );
} }

View file

@ -2,13 +2,13 @@ import 'dart:async';
import 'package:cached_network_image/cached_network_image.dart'; import 'package:cached_network_image/cached_network_image.dart';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:contacts_plus_plus/auxiliary.dart'; import 'package:recon/auxiliary.dart';
import 'package:contacts_plus_plus/clients/inventory_client.dart'; import 'package:recon/clients/inventory_client.dart';
import 'package:contacts_plus_plus/models/inventory/neos_path.dart'; import 'package:recon/models/inventory/resonite_directory.dart';
import 'package:contacts_plus_plus/models/records/record.dart'; import 'package:recon/models/records/record.dart';
import 'package:contacts_plus_plus/widgets/default_error_widget.dart'; import 'package:recon/widgets/default_error_widget.dart';
import 'package:contacts_plus_plus/widgets/inventory/object_inventory_tile.dart'; import 'package:recon/widgets/inventory/object_inventory_tile.dart';
import 'package:contacts_plus_plus/widgets/inventory/path_inventory_tile.dart'; import 'package:recon/widgets/inventory/path_inventory_tile.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:photo_view/photo_view.dart'; import 'package:photo_view/photo_view.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
@ -37,7 +37,7 @@ class _InventoryBrowserState extends State<InventoryBrowser> with AutomaticKeepA
Widget build(BuildContext context) { Widget build(BuildContext context) {
super.build(context); super.build(context);
return Consumer<InventoryClient>(builder: (BuildContext context, InventoryClient iClient, Widget? child) { return Consumer<InventoryClient>(builder: (BuildContext context, InventoryClient iClient, Widget? child) {
return FutureBuilder<NeosDirectory>( return FutureBuilder<ResoniteDirectory>(
future: iClient.directoryFuture, future: iClient.directoryFuture,
builder: (context, snapshot) { builder: (context, snapshot) {
final currentDir = snapshot.data; final currentDir = snapshot.data;
@ -57,7 +57,9 @@ class _InventoryBrowserState extends State<InventoryBrowser> with AutomaticKeepA
await iClient.reloadCurrentDirectory(); await iClient.reloadCurrentDirectory();
_refreshLimiter = Timer(_refreshLimit, () {}); _refreshLimiter = Timer(_refreshLimit, () {});
} catch (e) { } catch (e) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text("Refresh failed: $e"))); if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text("Refresh failed: $e")));
}
} }
}, },
child: Builder( child: Builder(
@ -179,7 +181,7 @@ class _InventoryBrowserState extends State<InventoryBrowser> with AutomaticKeepA
builder: (context) => PhotoView( builder: (context) => PhotoView(
minScale: PhotoViewComputedScale.contained, minScale: PhotoViewComputedScale.contained,
imageProvider: imageProvider:
CachedNetworkImageProvider(Aux.neosDbToHttp(record.thumbnailUri)), CachedNetworkImageProvider(Aux.resdbToHttp(record.thumbnailUri)),
heroAttributes: PhotoViewHeroAttributes(tag: record.id), heroAttributes: PhotoViewHeroAttributes(tag: record.id),
), ),
), ),

View file

@ -1,10 +1,11 @@
import 'dart:isolate'; import 'dart:isolate';
import 'dart:ui'; import 'dart:ui';
import 'package:contacts_plus_plus/auxiliary.dart'; import 'package:recon/auxiliary.dart';
import 'package:contacts_plus_plus/clients/inventory_client.dart'; import 'package:recon/clients/inventory_client.dart';
import 'package:file_picker/file_picker.dart'; import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_downloader/flutter_downloader.dart'; import 'package:flutter_downloader/flutter_downloader.dart';
import 'package:path/path.dart'; import 'package:path/path.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
@ -60,10 +61,16 @@ class _InventoryBrowserAppBarState extends State<InventoryBrowserAppBar> {
? AppBar( ? AppBar(
key: const ValueKey("default-appbar"), key: const ValueKey("default-appbar"),
title: const Text("Inventory"), title: const Text("Inventory"),
systemOverlayStyle: SystemUiOverlayStyle(
systemNavigationBarColor: Theme.of(context).navigationBarTheme.backgroundColor,
),
) )
: AppBar( : AppBar(
key: const ValueKey("selection-appbar"), key: const ValueKey("selection-appbar"),
title: Text("${iClient.selectedRecordCount} Selected"), title: Text("${iClient.selectedRecordCount} Selected"),
systemOverlayStyle: SystemUiOverlayStyle(
systemNavigationBarColor: Theme.of(context).navigationBarTheme.backgroundColor,
),
leading: IconButton( leading: IconButton(
onPressed: () { onPressed: () {
iClient.clearSelectedRecords(); iClient.clearSelectedRecords();
@ -142,7 +149,7 @@ class _InventoryBrowserAppBarState extends State<InventoryBrowserAppBar> {
for (var record in selectedRecords) { for (var record in selectedRecords) {
final uri = selectedUris == thumbUris ? record.thumbnailUri : record.thumbnailUri; final uri = selectedUris == thumbUris ? record.thumbnailUri : record.thumbnailUri;
await FlutterDownloader.enqueue( await FlutterDownloader.enqueue(
url: Aux.neosDbToHttp(uri), url: Aux.resdbToHttp(uri),
savedDir: directory, savedDir: directory,
showNotification: true, showNotification: true,
openFileFromNotification: false, openFileFromNotification: false,

View file

@ -1,6 +1,6 @@
import 'package:cached_network_image/cached_network_image.dart'; import 'package:cached_network_image/cached_network_image.dart';
import 'package:contacts_plus_plus/auxiliary.dart'; import 'package:recon/auxiliary.dart';
import 'package:contacts_plus_plus/models/records/record.dart'; import 'package:recon/models/records/record.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
@ -42,7 +42,7 @@ class ObjectInventoryTile extends StatelessWidget {
child: CachedNetworkImage( child: CachedNetworkImage(
height: double.infinity, height: double.infinity,
width: double.infinity, width: double.infinity,
imageUrl: Aux.neosDbToHttp(record.thumbnailUri), imageUrl: Aux.resdbToHttp(record.thumbnailUri),
fit: BoxFit.cover, fit: BoxFit.cover,
errorWidget: (context, url, error) => const Center( errorWidget: (context, url, error) => const Center(
child: Icon( child: Icon(

Some files were not shown because too many files have changed in this diff Show more