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
@ -17,4 +17,4 @@ For example, voice-messages and notifications are currently not supported on des
## 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 {
applicationId "me.voidspace.contacts_plus_plus"
applicationId "me.voidspace.recon"
minSdkVersion 21
targetSdkVersion flutter.targetSdkVersion
versionCode flutterVersionCode.toInteger()

View file

@ -1,5 +1,5 @@
<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 Flutter tool needs it to communicate with the running application
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"
package="de.voidspace.contacts_plus_plus">
package="de.voidspace.recon">
<!-- Required to fetch data from the internet. -->
<uses-permission android:name="android.permission.INTERNET" />
@ -7,7 +7,7 @@
<!-- Optional, you'll have to check this permission by yourself. -->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<application
android:label="Contacts++"
android:label="ReCon"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher">
<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

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">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
<monochrome android:drawable="@mipmap/ic_launcher_mono" />
</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"?>
<resources>
<color name="ic_launcher_background">#232426</color>
<color name="ic_launcher_background">#050505</color>
</resources>

View file

@ -1,5 +1,5 @@
<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 Flutter tool needs it to communicate with the running application
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 'package:contacts_plus_plus/clients/api_client.dart';
import 'package:contacts_plus_plus/models/cloud_variable.dart';
import 'package:recon/clients/api_client.dart';
import 'package:recon/models/cloud_variable.dart';
class CloudVariableApi {
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 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 "";
final body = jsonDecode(response.body);
return body["tag_name"] ?? "";

View file

@ -1,7 +1,7 @@
import 'dart:convert';
import 'package:contacts_plus_plus/clients/api_client.dart';
import 'package:contacts_plus_plus/models/message.dart';
import 'package:recon/clients/api_client.dart';
import 'package:recon/models/message.dart';
class MessageApi {
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:typed_data';
import 'package:collection/collection.dart';
import 'package:contacts_plus_plus/models/records/asset_digest.dart';
import 'package:contacts_plus_plus/models/records/json_template.dart';
import 'package:recon/models/records/asset_digest.dart';
import 'package:recon/models/records/json_template.dart';
import 'package:http/http.dart' as http;
import 'package:flutter/material.dart';
import 'package:contacts_plus_plus/clients/api_client.dart';
import 'package:contacts_plus_plus/models/records/asset_upload_data.dart';
import 'package:contacts_plus_plus/models/records/neos_db_asset.dart';
import 'package:contacts_plus_plus/models/records/preprocess_status.dart';
import 'package:contacts_plus_plus/models/records/record.dart';
import 'package:recon/clients/api_client.dart';
import 'package:recon/models/records/asset_upload_data.dart';
import 'package:recon/models/records/resonite_db_asset.dart';
import 'package:recon/models/records/preprocess_status.dart';
import 'package:recon/models/records/record.dart';
import 'package:http_parser/http_parser.dart';
import 'package:path/path.dart';
@ -68,7 +68,7 @@ class RecordApi {
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");
client.checkResponse(response);
final body = jsonDecode(response.body);
@ -84,7 +84,7 @@ class RecordApi {
}
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++) {
progressCallback?.call(i/uploadData.totalChunks);
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");
client.checkResponse(response);
}

View file

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

View file

@ -1,13 +1,9 @@
import 'dart:convert';
import 'package:contacts_plus_plus/clients/api_client.dart';
import 'package:contacts_plus_plus/models/users/friend.dart';
import 'package:contacts_plus_plus/models/personal_profile.dart';
import 'package:contacts_plus_plus/models/users/user.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';
import 'package:recon/clients/api_client.dart';
import 'package:recon/models/personal_profile.dart';
import 'package:recon/models/users/user.dart';
import 'package:recon/models/users/user_status.dart';
class UserApi {
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 {
return UserStatus.empty();
final response = await client.get("/users/$userId/status");
client.checkResponse(response);
final data = jsonDecode(response.body);
@ -32,18 +29,7 @@ class UserApi {
}
static Future<void> notifyOnlineInstance(ApiClient client) async {
final response = await client.post("/stats/instanceOnline/${client.authenticationData.secretMachineId.hashCode}");
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);
final response = await client.post("/stats/instanceOnline/${client.authenticationData.secretMachineIdHash}");
client.checkResponse(response);
}
@ -53,24 +39,4 @@ class UserApi {
final data = jsonDecode(response.body);
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:path/path.dart' as p;
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 {
static String neosDbToHttp(String? neosdb) {
if (neosdb == null || neosdb.isEmpty) return "";
if (neosdb.startsWith("http")) return neosdb;
final filename = p.basenameWithoutExtension(neosdb);
return "${Config.neosCdnUrl}$filename";
static String resdbToHttp(String? resdb) {
if (resdb == null || resdb.isEmpty) return "";
if (resdb.startsWith("http")) return resdb;
final filename = p.basenameWithoutExtension(resdb);
return "${Config.skyfrostAssetsUrl}/$filename";
}
}

View file

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

View file

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

View file

@ -1,9 +1,9 @@
import 'dart:io';
import 'package:contacts_plus_plus/auxiliary.dart';
import 'package:contacts_plus_plus/clients/api_client.dart';
import 'package:recon/auxiliary.dart';
import 'package:recon/clients/api_client.dart';
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_provider/path_provider.dart';
@ -15,7 +15,7 @@ class AudioCacheClient {
final file = File("${directory.path}/${basename(clip.assetUri)}");
if (!await file.exists()) {
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);
await file.writeAsBytes(response.bodyBytes);
}

View file

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

View file

@ -1,50 +1,25 @@
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:hive_flutter/hive_flutter.dart';
import 'package:http/http.dart' as http;
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 {
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 _unreadSafeguardDuration = Duration(seconds: 120);
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 Map<String, MessageCache> _messageCache = {};
final Map<String, List<Message>> _unreads = {};
final Logger _logger = Logger("NeosHub");
final Logger _logger = Logger("Messaging");
final NotificationClient _notificationClient;
final HubManager _hubManager = HubManager();
final Map<String, Session> _sessionMap = {};
Friend? selectedFriend;
Timer? _notifyOnlineTimer;
Timer? _autoRefresh;
Timer? _unreadSafeguard;
int _attempts = 0;
WebSocket? _wsChannel;
bool _isConnecting = false;
String? _initStatus;
UserStatus _userStatus = UserStatus.initial();
UserStatus get userStatus => _userStatus;
MessagingClient({required ApiClient apiClient, required NotificationClient notificationClient})
: _apiClient = apiClient, _notificationClient = notificationClient {
: _apiClient = apiClient,
_notificationClient = notificationClient {
debugPrint("mClient created: $hashCode");
Hive.openBox(_messageBoxKey).then((box) async {
box.delete(_lastUpdateKey);
await refreshFriendsListWithErrorHandler();
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));
await box.delete(_lastUpdateKey);
_setupHub();
});
}
@ -89,21 +59,20 @@ class MessagingClient extends ChangeNotifier {
_autoRefresh?.cancel();
_notifyOnlineTimer?.cancel();
_unreadSafeguard?.cancel();
_wsChannel?.close();
_hubManager.dispose();
super.dispose();
}
String? get initStatus => _initStatus;
bool get websocketConnected => _wsChannel != null;
List<Friend> get cachedFriends => _sortedFriendsCache;
List<Message> getUnreadsForFriend(Friend friend) => _unreads[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));
@ -111,7 +80,6 @@ class MessagingClient extends ChangeNotifier {
MessageCache _createUserMessageCache(String userId) => MessageCache(apiClient: _apiClient, userId: userId);
Future<void> refreshFriendsListWithErrorHandler() async {
try {
await refreshFriendsList();
@ -126,25 +94,18 @@ class MessagingClient extends ChangeNotifier {
_autoRefresh?.cancel();
_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) {
await _updateFriend(friend);
await _updateContact(friend);
}
_initStatus = "";
notifyListeners();
}
void sendMessage(Message message) async {
void sendMessage(Message message) {
final msgBody = message.toMap();
final data = {
"type": EventType.message.index,
"target": "SendMessage",
"arguments": [
msgBody
],
};
_sendData(data);
_hubManager.send("SendMessage", arguments: [msgBody]);
final cache = getUserMessageCache(message.recipientId) ?? _createUserMessageCache(message.recipientId);
cache.addMessage(message);
notifyListeners();
@ -152,17 +113,37 @@ class MessagingClient extends ChangeNotifier {
void markMessagesRead(MarkReadBatch batch) {
final msgBody = batch.toMap();
final data = {
"type": EventType.message.index,
"target": "MarkMessagesRead",
"arguments": [
msgBody
],
};
_sendData(data);
_hubManager.send("MarkMessagesRead", arguments: [msgBody]);
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) {
var messages = _unreads[message.senderId];
if (messages == null) {
@ -211,7 +192,7 @@ class MessagingClient extends ChangeNotifier {
final friend = getAsFriend(userId);
if (friend == null) return;
final newStatus = await UserApi.getUserStatus(_apiClient, userId: userId);
await _updateFriend(friend.copyWith(userStatus: newStatus));
await _updateContact(friend.copyWith(userStatus: newStatus));
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);
box.put(friend.id, friend.toMap());
final lastStatusUpdate = box.get(_lastUpdateKey);
@ -257,108 +238,52 @@ class MessagingClient extends ChangeNotifier {
_sortFriendsCache();
}
// ===== Websocket Stuff =====
void _onDisconnected(error) async {
_wsChannel = null;
_logger.warning("Neos Hub connection died with error '$error', reconnecting...");
await _startWebsocket();
}
Future<void> _startWebsocket() async {
Future<void> _setupHub() async {
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;
}
if (_isConnecting) {
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);
}
_hubManager.setHeaders(_apiClient.authorizationHeader);
Future<WebSocket> _tryConnect() async {
while (true) {
try {
final http.Response response;
try {
response = await http.post(
Uri.parse("${Config.neosHubUrl}/negotiate"),
headers: _apiClient.authorizationHeader,
_hubManager.setHandler(EventTarget.messageSent, _onMessageSent);
_hubManager.setHandler(EventTarget.receiveMessage, _onReceiveMessage);
_hubManager.setHandler(EventTarget.messagesRead, _onMessagesRead);
_hubManager.setHandler(EventTarget.receiveStatusUpdate, _onReceiveStatusUpdate);
_hubManager.setHandler(EventTarget.receiveSessionUpdate, _onReceiveSessionUpdate);
_hubManager.setHandler(EventTarget.removeSession, _onRemoveSession);
await _hubManager.start();
await setUserStatus(userStatus);
_hubManager.send(
"InitializeStatus",
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);
}
_initStatus = "";
notifyListeners();
await _refreshUnreads();
_unreadSafeguard = Timer.periodic(_unreadSafeguardDuration, (timer) => _refreshUnreads());
_hubManager.send("RequestStatus", arguments: [null, false]);
},
);
_apiClient.checkResponse(response);
} catch (e) {
throw "Failed to acquire connection info from Neos API: $e";
}
final body = jsonDecode(response.body);
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;
}
Map<String, Session> createSessionMap(String salt) {
return _sessionMap.map((key, value) => MapEntry(CryptoHelper.idHash(value.id + salt), value));
}
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:
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();
break;
case EventTarget.receiveMessage:
}
void _onReceiveMessage(List args) {
final msg = args[0];
final message = Message.fromMap(msg);
final cache = getUserMessageCache(message.senderId) ?? _createUserMessageCache(message.senderId);
@ -370,23 +295,43 @@ class MessagingClient extends ChangeNotifier {
markMessagesRead(MarkReadBatch(senderId: message.senderId, ids: [message.id], readTime: DateTime.now()));
}
notifyListeners();
break;
case EventTarget.messagesRead:
}
void _onMessagesRead(List args) {
final messageIds = args[0]["ids"] as List;
final recipientId = args[0]["recipientId"];
if (recipientId == null) break;
if (recipientId == null) return;
final cache = getUserMessageCache(recipientId);
if (cache == null) break;
if (cache == null) return;
for (var id in messageIds) {
cache.setMessageState(id, MessageState.read);
}
notifyListeners();
break;
}
}
void _sendData(data) {
if (_wsChannel == null) throw "Neos Hub is not connected";
_wsChannel!.add(jsonEncode(data)+_eofChar);
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 'package:contacts_plus_plus/auxiliary.dart';
import 'package:contacts_plus_plus/models/message.dart';
import 'package:contacts_plus_plus/models/session.dart';
import 'package:recon/auxiliary.dart';
import 'package:recon/models/message.dart';
import 'package:recon/models/session.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart' as fln;
import 'package:collection/collection.dart';

View file

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

View file

@ -1,6 +1,6 @@
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';

View file

@ -1,13 +1,9 @@
class Config {
static const String apiBaseUrl = "https://api.neos.com";
static const String legacyCloudUrl = "https://neoscloud.blob.core.windows.net/assets/";
static const String blobStorageUrl = "https://cloudxstorage.blob.core.windows.net/assets/";
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 String apiBaseUrl = "https://api.resonite.com";
static const String skyfrostAssetsUrl = "https://assets.resonite.com";
static const String resoniteHubUrl = "$apiBaseUrl/hub";
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 'package:contacts_plus_plus/apis/github_api.dart';
import 'package:contacts_plus_plus/client_holder.dart';
import 'package:contacts_plus_plus/clients/api_client.dart';
import 'package:contacts_plus_plus/clients/inventory_client.dart';
import 'package:contacts_plus_plus/clients/messaging_client.dart';
import 'package:contacts_plus_plus/clients/session_client.dart';
import 'package:contacts_plus_plus/clients/settings_client.dart';
import 'package:contacts_plus_plus/models/sem_ver.dart';
import 'package:contacts_plus_plus/widgets/homepage.dart';
import 'package:contacts_plus_plus/widgets/login_screen.dart';
import 'package:contacts_plus_plus/widgets/update_notifier.dart';
import 'package:recon/apis/github_api.dart';
import 'package:recon/client_holder.dart';
import 'package:recon/clients/api_client.dart';
import 'package:recon/clients/inventory_client.dart';
import 'package:recon/clients/messaging_client.dart';
import 'package:recon/clients/session_client.dart';
import 'package:recon/clients/settings_client.dart';
import 'package:recon/models/sem_ver.dart';
import 'package:recon/widgets/homepage.dart';
import 'package:recon/widgets/login_screen.dart';
import 'package:recon/widgets/update_notifier.dart';
import 'package:dynamic_color/dynamic_color.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_downloader/flutter_downloader.dart';
import 'package:flutter_phoenix/flutter_phoenix.dart';
import 'package:hive_flutter/hive_flutter.dart';
@ -47,20 +48,20 @@ void main() async {
cachedAuth = await ApiClient.tryCachedLogin();
} catch (_) {}
runApp(ContactsPlusPlus(settingsClient: settingsClient, cachedAuthentication: cachedAuth));
runApp(ReCon(settingsClient: settingsClient, cachedAuthentication: cachedAuth));
}
class ContactsPlusPlus extends StatefulWidget {
const ContactsPlusPlus({required this.settingsClient, required this.cachedAuthentication, super.key});
class ReCon extends StatefulWidget {
const ReCon({required this.settingsClient, required this.cachedAuthentication, super.key});
final SettingsClient settingsClient;
final AuthenticationData cachedAuthentication;
@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);
late AuthenticationData _authData = widget.cachedAuthentication;
bool _checkedForUpdate = false;
@ -128,7 +129,7 @@ class _ContactsPlusPlusState extends State<ContactsPlusPlus> {
child: DynamicColorBuilder(
builder: (ColorScheme? lightDynamic, ColorScheme? darkDynamic) => MaterialApp(
debugShowCheckedModeBanner: false,
title: 'Contacts++',
title: 'ReCon',
theme: ThemeData(
useMaterial3: true,
textTheme: _typography.black,

View file

@ -1,35 +1,50 @@
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 token;
final String secretMachineId;
final String secretMachineIdHash;
final bool isAuthenticated;
final String uid;
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) {
map = map["entity"];
final userId = map["userId"];
final token = map["token"];
final machineId = map["secretMachineId"];
if (userId == null || token == null || machineId == null) {
final machineId = map["secretMachineIdHash"];
final uid = map["uid"];
if (userId == null || token == null || machineId == null || uid == null) {
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;
Map<String, String> get authorizationHeader => {
"Authorization": "neos $userId:$token"
"Authorization": "res $userId:$token",
"UID": uid,
};
Map<String, dynamic> toMap() {
return {
"userId": userId,
"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 {
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:developer';
import 'package:contacts_plus_plus/clients/api_client.dart';
import 'package:contacts_plus_plus/apis/message_api.dart';
import 'package:contacts_plus_plus/auxiliary.dart';
import 'package:contacts_plus_plus/string_formatter.dart';
import 'package:recon/clients/api_client.dart';
import 'package:recon/apis/message_api.dart';
import 'package:recon/auxiliary.dart';
import 'package:recon/string_formatter.dart';
import 'package:uuid/uuid.dart';
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 {
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 bool isUploaded;

View file

@ -1,25 +1,25 @@
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';
class AssetDigest {
final Uint8List data;
final NeosDBAsset asset;
final ResoniteDBAsset asset;
final String name;
final String dbUri;
AssetDigest({required this.data, required this.asset, required this.name, required this.dbUri});
static Future<AssetDigest> fromData(Uint8List data, String filename) async {
final asset = NeosDBAsset.fromData(data);
final asset = ResoniteDBAsset.fromData(data);
return AssetDigest(
data: data,
asset: asset,
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';
class JsonTemplate {
static const String thumbUrl = "neosdb:///8ed80703e48c3d1556093927b67298f3d5e10315e9f782ec56fc49d6366f09b7.webp";
static const String thumbUrl = "resdb:///8ed80703e48c3d1556093927b67298f3d5e10315e9f782ec56fc49d6366f09b7.webp";
final Map data;
JsonTemplate({required this.data});
@ -2371,7 +2371,7 @@ class JsonTemplate {
},
"URL": {
"ID": const Uuid().v4(),
"Data": "@neosdb:///3738bf6fc560f7d08d872ce12b06f4d9337ac5da415b6de6008a49ca128658ec"
"Data": "@resdb:///3738bf6fc560f7d08d872ce12b06f4d9337ac5da415b6de6008a49ca128658ec"
},
"Readable": {
"ID": const Uuid().v4(),
@ -2444,7 +2444,7 @@ class JsonTemplate {
},
"URL": {
"ID": const Uuid().v4(),
"Data": "@neosdb:///c801b8d2522fb554678f17f4597158b1af3f9be3abd6ce35d5a3112a81e2bf39"
"Data": "@resdb:///c801b8d2522fb554678f17f4597158b1af3f9be3abd6ce35d5a3112a81e2bf39"
},
"Padding": {
"ID": const Uuid().v4(),
@ -2478,7 +2478,7 @@ class JsonTemplate {
},
"URL": {
"ID": const Uuid().v4(),
"Data": "@neosdb:///4cac521169034ddd416c6deffe2eb16234863761837df677a910697ec5babd25"
"Data": "@resdb:///4cac521169034ddd416c6deffe2eb16234863761837df677a910697ec5babd25"
},
"Padding": {
"ID": const Uuid().v4(),
@ -2512,7 +2512,7 @@ class JsonTemplate {
},
"URL": {
"ID": const Uuid().v4(),
"Data": "@neosdb:///23e7ad7cb0a5a4cf75e07c9e0848b1eb06bba15e8fa9b8cb0579fc823c532927"
"Data": "@resdb:///23e7ad7cb0a5a4cf75e07c9e0848b1eb06bba15e8fa9b8cb0579fc823c532927"
},
"Padding": {
"ID": const Uuid().v4(),
@ -2546,7 +2546,7 @@ class JsonTemplate {
},
"URL": {
"ID": const Uuid().v4(),
"Data": "@neosdb:///415dc6290378574135b64c808dc640c1df7531973290c4970c51fdeb849cb0c5"
"Data": "@resdb:///415dc6290378574135b64c808dc640c1df7531973290c4970c51fdeb849cb0c5"
},
"Padding": {
"ID": const Uuid().v4(),
@ -2580,7 +2580,7 @@ class JsonTemplate {
},
"URL": {
"ID": const Uuid().v4(),
"Data": "@neosdb:///bcda0bcc22bab28ea4fedae800bfbf9ec76d71cc3b9f851779a35b7e438a839d"
"Data": "@resdb:///bcda0bcc22bab28ea4fedae800bfbf9ec76d71cc3b9f851779a35b7e438a839d"
},
"Padding": {
"ID": const Uuid().v4(),
@ -2720,7 +2720,7 @@ class JsonTemplate {
},
"URL": {
"ID": const Uuid().v4(),
"Data": "@neosdb:///274f0d4ea4bce93abc224c9ae9f9a97a9a396b382c5338f71c738d1591dd5c35.webp"
"Data": "@resdb:///274f0d4ea4bce93abc224c9ae9f9a97a9a396b382c5338f71c738d1591dd5c35.webp"
},
"FilterMode": {
"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
{

View file

@ -1,10 +1,9 @@
import 'package:contacts_plus_plus/auxiliary.dart';
import 'package:contacts_plus_plus/models/message.dart';
import 'package:contacts_plus_plus/models/records/asset_digest.dart';
import 'package:contacts_plus_plus/models/records/neos_db_asset.dart';
import 'package:contacts_plus_plus/string_formatter.dart';
import 'package:recon/auxiliary.dart';
import 'package:recon/models/message.dart';
import 'package:recon/models/records/asset_digest.dart';
import 'package:recon/models/records/resonite_db_asset.dart';
import 'package:recon/string_formatter.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:uuid/uuid.dart';
enum RecordType {
@ -59,7 +58,7 @@ class Record {
isListed: false,
isForPatreons: false,
lastModificationTime: DateTimeX.epoch,
neosDBManifest: [],
resoniteDBManifest: [],
lastModifyingUserId: "",
lastModifyingMachineId: "",
creationTime: DateTimeX.epoch,
@ -100,7 +99,7 @@ class Record {
final int rating;
final int randomOrder;
final List<String> manifest;
final List<NeosDBAsset> neosDBManifest;
final List<ResoniteDBAsset> resoniteDBManifest;
final String url;
final bool isValidOwnerId;
final bool isValidRecordId;
@ -122,7 +121,7 @@ class Record {
required this.isListed,
required this.isForPatreons,
required this.lastModificationTime,
required this.neosDBManifest,
required this.resoniteDBManifest,
required this.lastModifyingUserId,
required this.lastModifyingMachineId,
required this.creationTime,
@ -153,14 +152,14 @@ class Record {
combinedRecordId: combinedRecordId,
assetUri: assetUri,
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(),
recordType: recordType,
thumbnailUri: thumbnailUri,
isPublic: false,
isForPatreons: false,
isListed: false,
neosDBManifest: digests.map((e) => e.asset).toList(),
resoniteDBManifest: digests.map((e) => e.asset).toList(),
globalVersion: 0,
localVersion: 1,
lastModifyingUserId: userId,
@ -173,7 +172,7 @@ class Record {
path: '',
description: '',
manifest: digests.map((e) => e.dbUri).toList(),
url: "neosrec:///$userId/${combinedRecordId.id}",
url: "resrec:///$userId/${combinedRecordId.id}",
isValidOwnerId: true,
isValidRecordId: true,
visits: 0,
@ -199,14 +198,14 @@ class Record {
isForPatreons: map["isForPatreons"] ?? false,
isListed: map["isListed"] ?? false,
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"] ?? "",
lastModifyingMachineId: map["lastModifyingMachineId"] ?? "",
creationTime: DateTime.tryParse(map["lastModificationTime"]) ?? DateTimeX.epoch,
isSynced: map["isSynced"] ?? false,
fetchedOn: DateTime.tryParse(map["fetchedOn"] ?? "") ?? DateTimeX.epoch,
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"] ?? "",
isValidOwnerId: map["isValidOwnerId"] == "true",
isValidRecordId: map["isValidRecordId"] == "true",
@ -220,7 +219,7 @@ class Record {
bool get isRoot => this == _rootRecord;
String get linkRecordId {
if (!assetUri.startsWith("neosrec")) {
if (!assetUri.startsWith("resrec")) {
throw "Record is not a link.";
}
@ -233,11 +232,11 @@ class Record {
}
String get linkOwnerId {
if (!assetUri.startsWith("neosrec")) {
if (!assetUri.startsWith("resrec")) {
throw "Record is not a link.";
}
String ownerId = assetUri.replaceFirst("neosrec:///", "");
String ownerId = assetUri.replaceFirst("resrec:///", "");
final lastSlashIdx = ownerId.lastIndexOf("/");
if (lastSlashIdx == -1) {
@ -265,7 +264,7 @@ class Record {
bool? isListed,
bool? isDeleted,
DateTime? lastModificationTime,
List<NeosDBAsset>? neosDBManifest,
List<ResoniteDBAsset>? resoniteDBManifest,
String? lastModifyingUserId,
String? lastModifyingMachineId,
DateTime? creationTime,
@ -296,7 +295,7 @@ class Record {
isForPatreons: isForPatreons ?? this.isForPatreons,
isListed: isListed ?? this.isListed,
lastModificationTime: lastModificationTime ?? this.lastModificationTime,
neosDBManifest: neosDBManifest ?? this.neosDBManifest,
resoniteDBManifest: resoniteDBManifest ?? this.resoniteDBManifest,
lastModifyingUserId: lastModifyingUserId ?? this.lastModifyingUserId,
lastModifyingMachineId: lastModifyingMachineId ?? this.lastModifyingMachineId,
creationTime: creationTime ?? this.creationTime,
@ -330,7 +329,7 @@ class Record {
"isForPatreons": isForPatreons,
"isListed": isListed,
"lastModificationTime": lastModificationTime.toUtc().toIso8601String(),
"neosDBManifest": neosDBManifest.map((e) => e.toMap()).toList(),
"resoniteDBManifest": resoniteDBManifest.map((e) => e.toMap()).toList(),
"lastModifyingUserId": lastModifyingUserId,
"lastModifyingMachineId": lastModifyingMachineId,
"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 'package:contacts_plus_plus/string_formatter.dart';
import 'dart:convert';
import 'package:recon/string_formatter.dart';
import 'package:crypto/crypto.dart';
class Session {
final String id;
final String name;
final FormatNode formattedName;
final List<SessionUser> sessionUsers;
final String thumbnail;
final String thumbnailUrl;
final int maxUsers;
final bool hasEnded;
final bool isValid;
@ -22,7 +24,7 @@ class Session {
required this.id,
required this.name,
required this.sessionUsers,
required this.thumbnail,
required this.thumbnailUrl,
required this.maxUsers,
required this.hasEnded,
required this.isValid,
@ -40,7 +42,7 @@ class Session {
id: "",
name: "",
sessionUsers: const [],
thumbnail: "",
thumbnailUrl: "",
maxUsers: 0,
hasEnded: true,
isValid: false,
@ -60,7 +62,7 @@ class Session {
id: map["sessionId"],
name: map["name"],
sessionUsers: (map["sessionUsers"] as List? ?? []).map((entry) => SessionUser.fromMap(entry)).toList(),
thumbnail: map["thumbnail"] ?? "",
thumbnailUrl: map["thumbnailUrl"] ?? "",
maxUsers: map["maxUsers"] ?? 0,
hasEnded: map["hasEnded"] ?? false,
isValid: map["isValid"] ?? true,
@ -78,7 +80,7 @@ class Session {
"sessionId": id,
"name": name,
"sessionUsers": shallow ? [] : sessionUsers.map((e) => e.toMap()).toList(),
"thumbnail": thumbnail,
"thumbnail": thumbnailUrl,
"maxUsers": maxUsers,
"hasEnded": hasEnded,
"isValid": isValid,
@ -97,15 +99,17 @@ class Session {
enum SessionAccessLevel {
unknown,
private,
friends,
friendsOfFriends,
contacts,
contactsPlus,
registeredUsers,
anyone;
static const _readableNamesMap = {
SessionAccessLevel.unknown: "Unknown",
SessionAccessLevel.private: "Private",
SessionAccessLevel.friends: "Contacts",
SessionAccessLevel.friendsOfFriends: "Contacts+",
SessionAccessLevel.contacts: "Contacts",
SessionAccessLevel.contactsPlus: "Contacts+",
SessionAccessLevel.registeredUsers: "Registered users",
SessionAccessLevel.anyone: "Anyone",
};
@ -117,7 +121,7 @@ enum SessionAccessLevel {
}
String toReadableString() {
return SessionAccessLevel._readableNamesMap[this] ?? "Unknown";
return SessionAccessLevel._readableNamesMap[this] ?? SessionAccessLevel.unknown.toReadableString();
}
}
@ -177,7 +181,6 @@ class SessionFilterSettings {
String buildRequestString() => "?includeEmptyHeadless=$includeEmptyHeadless"
"${"&includeEnded=$includeEnded"}"
"${name.isNotEmpty ? "&name=$name" : ""}"
"${!includeIncompatible ? "&compatibilityHash=${Uri.encodeComponent(Config.latestCompatHash)}" : ""}"
"${hostName.isNotEmpty ? "&hostName=$hostName" : ""}"
"${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 'package:contacts_plus_plus/models/sem_ver.dart';
import 'package:contacts_plus_plus/models/users/online_status.dart';
import 'package:recon/models/sem_ver.dart';
import 'package:recon/models/users/online_status.dart';
import 'package:flutter/material.dart';
import 'package:uuid/uuid.dart';

View file

@ -1,36 +1,36 @@
import 'package:contacts_plus_plus/auxiliary.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/online_status.dart';
import 'package:contacts_plus_plus/models/users/user_status.dart';
import 'package:recon/auxiliary.dart';
import 'package:recon/models/users/user_profile.dart';
import 'package:recon/models/users/friend_status.dart';
import 'package:recon/models/users/online_status.dart';
import 'package:recon/models/users/user_status.dart';
class Friend implements Comparable {
static const _emptyId = "-1";
static const _neosBotId = "U-Neos";
static const _resoniteBotId = "U-Resonite";
final String id;
final String username;
final String ownerId;
final UserStatus userStatus;
final UserProfile userProfile;
final FriendStatus friendStatus;
final FriendStatus contactStatus;
final DateTime latestMessageTime;
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) {
final userStatus = UserStatus.fromMap(map["userStatus"]);
var userStatus = map["userStatus"] == null ? UserStatus.empty() : UserStatus.fromMap(map["userStatus"]);
return Friend(
id: map["id"],
username: map["friendUsername"],
username: map["contactUsername"],
ownerId: map["ownerId"] ?? map["id"],
// 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"] ?? {}),
friendStatus: FriendStatus.fromString(map["friendStatus"]),
contactStatus: FriendStatus.fromString(map["contactStatus"]),
latestMessageTime: map["latestMessageTime"] == null
? DateTime.fromMillisecondsSinceEpoch(0) : DateTime.parse(map["latestMessageTime"]),
);
@ -48,7 +48,7 @@ class Friend implements Comparable {
ownerId: "",
userStatus: UserStatus.empty(),
userProfile: UserProfile.empty(),
friendStatus: FriendStatus.none,
contactStatus: FriendStatus.none,
latestMessageTime: DateTimeX.epoch
);
}
@ -57,14 +57,14 @@ class Friend implements Comparable {
Friend copyWith({
String? id, String? username, String? ownerId, UserStatus? userStatus, UserProfile? userProfile,
FriendStatus? friendStatus, DateTime? latestMessageTime}) {
FriendStatus? contactStatus, DateTime? latestMessageTime}) {
return Friend(
id: id ?? this.id,
username: username ?? this.username,
ownerId: ownerId ?? this.ownerId,
userStatus: userStatus ?? this.userStatus,
userProfile: userProfile ?? this.userProfile,
friendStatus: friendStatus ?? this.friendStatus,
contactStatus: contactStatus ?? this.contactStatus,
latestMessageTime: latestMessageTime ?? this.latestMessageTime,
);
}
@ -72,11 +72,11 @@ class Friend implements Comparable {
Map toMap({bool shallow=false}) {
return {
"id": id,
"username": username,
"contactUsername": username,
"ownerId": ownerId,
"userStatus": userStatus.toMap(shallow: shallow),
"profile": userProfile.toMap(),
"friendStatus": friendStatus.name,
"contactStatus": contactStatus.name,
"latestMessageTime": latestMessageTime.toIso8601String(),
};
}

View file

@ -19,7 +19,7 @@ enum OnlineStatus {
factory OnlineStatus.fromString(String? text) {
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 {
final String id;

View file

@ -1,56 +1,100 @@
import 'package:contacts_plus_plus/models/session.dart';
import 'package:contacts_plus_plus/models/users/online_status.dart';
import 'package:recon/crypto_helper.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 {
final OnlineStatus onlineStatus;
final DateTime lastStatusChange;
final int currentSessionAccessLevel;
final bool currentSessionHidden;
final bool currentHosting;
final Session currentSession;
final List<Session> activeSessions;
final String neosVersion;
final DateTime lastPresenceTimestamp;
final String userSessionId;
final int currentSessionIndex;
final List<SessionMetadata> sessions;
final String appVersion;
final String outputDevice;
final bool isMobile;
final bool isPresent;
final String compatibilityHash;
final String hashSalt;
final UserSessionType sessionType;
final List<Session> decodedSessions;
const UserStatus(
{required this.onlineStatus, required this.lastStatusChange, required this.currentSession,
required this.currentSessionAccessLevel, required this.currentSessionHidden, required this.currentHosting,
required this.activeSessions, required this.neosVersion, required this.outputDevice, required this.isMobile,
const UserStatus({
required this.onlineStatus,
required this.lastStatusChange,
required this.lastPresenceTimestamp,
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() =>
UserStatus(
onlineStatus: OnlineStatus.offline,
lastStatusChange: DateTime.now(),
currentSessionAccessLevel: 0,
currentSessionHidden: false,
currentHosting: false,
currentSession: Session.none(),
activeSessions: [],
neosVersion: "",
lastPresenceTimestamp: DateTime.now(),
userSessionId: "",
currentSessionIndex: -1,
sessions: [],
appVersion: "",
outputDevice: "Unknown",
isMobile: false,
isPresent: false,
compatibilityHash: "",
hashSalt: "",
sessionType: UserSessionType.unknown
);
factory UserStatus.fromMap(Map map) {
final statusString = map["onlineStatus"] as String?;
final statusString = map["onlineStatus"].toString();
final status = OnlineStatus.fromString(statusString);
return UserStatus(
onlineStatus: status,
lastStatusChange: DateTime.parse(map["lastStatusChange"]),
currentSessionAccessLevel: map["currentSessionAccessLevel"] ?? 0,
currentSessionHidden: map["currentSessionHidden"] ?? false,
currentHosting: map["currentHosting"] ?? false,
currentSession: Session.fromMap(map["currentSession"]),
activeSessions: (map["activeSessions"] as List? ?? []).map((e) => Session.fromMap(e)).toList(),
neosVersion: map["neosVersion"] ?? "",
lastStatusChange: DateTime.tryParse(map["lastStatusChange"] ?? "") ?? DateTime.now(),
lastPresenceTimestamp: DateTime.tryParse(map["lastPresenceTimestamp"] ?? "") ?? DateTime.now(),
userSessionId: map["userSessionId"] ?? "",
isPresent: map["isPresent"] ?? false,
currentSessionIndex: map["currentSessionIndex"] ?? -1,
sessions: (map["sessions"] as List? ?? []).map((e) => SessionMetadata.fromMap(e)).toList(),
appVersion: map["appVersion"] ?? "",
outputDevice: map["outputDevice"] ?? "Unknown",
isMobile: map["isMobile"] ?? false,
compatibilityHash: map["compatabilityHash"] ?? ""
compatibilityHash: map["compatabilityHash"] ?? "",
hashSalt: map["hashSalt"] ?? "",
sessionType: UserSessionType.fromString(map["sessionType"])
);
}
@ -58,12 +102,18 @@ class UserStatus {
return {
"onlineStatus": onlineStatus.index,
"lastStatusChange": lastStatusChange.toIso8601String(),
"currentSessionAccessLevel": currentSessionAccessLevel,
"currentSessionHidden": currentSessionHidden,
"currentHosting": currentHosting,
"currentSession": currentSession.isNone || shallow ? null : currentSession.toMap(),
"activeSessions": shallow ? [] : activeSessions.map((e) => e.toMap(),).toList(),
"neosVersion": neosVersion,
"isPresent": isPresent,
"lastPresenceTimestamp": lastPresenceTimestamp.toIso8601String(),
"userSessionId": userSessionId,
"currentSessionIndex": currentSessionIndex,
"sessions": shallow
? []
: sessions
.map(
(e) => e.toMap(),
)
.toList(),
"appVersion": appVersion,
"outputDevice": outputDevice,
"isMobile": isMobile,
"compatibilityHash": compatibilityHash,
@ -73,27 +123,33 @@ class UserStatus {
UserStatus copyWith({
OnlineStatus? onlineStatus,
DateTime? lastStatusChange,
int? currentSessionAccessLevel,
bool? currentSessionHidden,
bool? currentHosting,
Session? currentSession,
List<Session>? activeSessions,
String? neosVersion,
DateTime? lastPresenceTimestamp,
bool? isPresent,
String? userSessionId,
int? currentSessionIndex,
List<SessionMetadata>? sessions,
String? appVersion,
String? outputDevice,
bool? isMobile,
String? compatibilityHash,
String? hashSalt,
UserSessionType? sessionType,
List<Session>? sessionData,
}) =>
UserStatus(
onlineStatus: onlineStatus ?? this.onlineStatus,
lastStatusChange: lastStatusChange ?? this.lastStatusChange,
currentSessionAccessLevel: currentSessionAccessLevel ?? this.currentSessionAccessLevel,
currentSessionHidden: currentSessionHidden ?? this.currentSessionHidden,
currentHosting: currentHosting ?? this.currentHosting,
currentSession: currentSession ?? this.currentSession,
activeSessions: activeSessions ?? this.activeSessions,
neosVersion: neosVersion ?? this.neosVersion,
lastPresenceTimestamp: lastPresenceTimestamp ?? this.lastPresenceTimestamp,
isPresent: isPresent ?? this.isPresent,
userSessionId: userSessionId ?? this.userSessionId,
currentSessionIndex: currentSessionIndex ?? this.currentSessionIndex,
sessions: sessions ?? this.sessions,
appVersion: appVersion ?? this.appVersion,
outputDevice: outputDevice ?? this.outputDevice,
isMobile: isMobile ?? this.isMobile,
compatibilityHash: compatibilityHash ?? this.compatibilityHash,
hashSalt: hashSalt ?? this.hashSalt,
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';
class FormattedText extends StatelessWidget {

View file

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

View file

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

View file

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

View file

@ -1,11 +1,10 @@
import 'package:contacts_plus_plus/apis/user_api.dart';
import 'package:contacts_plus_plus/client_holder.dart';
import 'package:contacts_plus_plus/clients/messaging_client.dart';
import 'package:contacts_plus_plus/models/users/online_status.dart';
import 'package:contacts_plus_plus/models/users/user_status.dart';
import 'package:contacts_plus_plus/widgets/friends/user_search.dart';
import 'package:contacts_plus_plus/widgets/my_profile_dialog.dart';
import 'package:recon/client_holder.dart';
import 'package:recon/clients/messaging_client.dart';
import 'package:recon/models/users/online_status.dart';
import 'package:recon/widgets/friends/user_search.dart';
import 'package:recon/widgets/my_profile_dialog.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:intl/intl.dart';
import 'package:provider/provider.dart';
@ -17,44 +16,17 @@ class FriendsListAppBar extends StatefulWidget {
}
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
Widget build(BuildContext context) {
super.build(context);
return AppBar(
title: const Text("Contacts++"),
title: const Text("ReCon"),
systemOverlayStyle: SystemUiOverlayStyle(
systemNavigationBarColor: Theme.of(context).navigationBarTheme.backgroundColor,
),
actions: [
FutureBuilder(
future: _userStatusFuture,
builder: (context, snapshot) {
if (snapshot.hasData) {
final userStatus = snapshot.data as UserStatus;
Consumer<MessagingClient>(builder: (context, client, _) {
return PopupMenuButton<OnlineStatus>(
child: Row(
children: [
@ -63,29 +35,25 @@ class _FriendsListAppBarState extends State<FriendsListAppBar> with AutomaticKee
child: Icon(
Icons.circle,
size: 16,
color: userStatus.onlineStatus.color(context),
color: client.userStatus.onlineStatus.color(context),
),
),
Text(toBeginningOfSentenceCase(userStatus.onlineStatus.name) ?? "Unknown"),
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 {
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));
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.")));
setState(() {
_userStatusFuture = Future.value(userStatus);
});
}
}
},
itemBuilder: (BuildContext context) => OnlineStatus.values
@ -109,41 +77,9 @@ class _FriendsListAppBarState extends State<FriendsListAppBar> with AutomaticKee
),
),
)
.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"),
.toList(),
);
} 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"),
);
}
},
),
}),
Padding(
padding: const EdgeInsets.only(left: 4, right: 4),
child: PopupMenuButton<MenuItemDefinition>(

View file

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

View file

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

View file

@ -1,11 +1,11 @@
import 'package:contacts_plus_plus/widgets/friends/friends_list.dart';
import 'package:contacts_plus_plus/widgets/friends/friends_list_app_bar.dart';
import 'package:contacts_plus_plus/widgets/inventory/inventory_browser.dart';
import 'package:contacts_plus_plus/widgets/inventory/inventory_browser_app_bar.dart';
import 'package:contacts_plus_plus/widgets/sessions/session_list.dart';
import 'package:contacts_plus_plus/widgets/sessions/session_list_app_bar.dart';
import 'package:contacts_plus_plus/widgets/settings_app_bar.dart';
import 'package:contacts_plus_plus/widgets/settings_page.dart';
import 'package:recon/widgets/friends/friends_list.dart';
import 'package:recon/widgets/friends/friends_list_app_bar.dart';
import 'package:recon/widgets/inventory/inventory_browser.dart';
import 'package:recon/widgets/inventory/inventory_browser_app_bar.dart';
import 'package:recon/widgets/sessions/session_list.dart';
import 'package:recon/widgets/sessions/session_list_app_bar.dart';
import 'package:recon/widgets/settings_app_bar.dart';
import 'package:recon/widgets/settings_page.dart';
import 'package:flutter/material.dart';
class Home extends StatefulWidget {
@ -29,6 +29,7 @@ class _HomeState extends State<Home> {
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Theme.of(context).colorScheme.surface,
appBar: PreferredSize(
preferredSize: const Size.fromHeight(kToolbarHeight),
child: AnimatedSwitcher(
@ -46,18 +47,9 @@ class _HomeState extends State<Home> {
SettingsPage(),
],
),
bottomNavigationBar: Container(
decoration: BoxDecoration(
border: const Border(top: BorderSide(width: 1, color: Colors.black)),
color: Theme.of(context).colorScheme.background,
),
child: BottomNavigationBar(
backgroundColor: Theme.of(context).colorScheme.surface,
type: BottomNavigationBarType.fixed,
unselectedItemColor: Theme.of(context).colorScheme.onBackground,
selectedItemColor: Theme.of(context).colorScheme.primary,
currentIndex: _selectedPage,
onTap: (index) {
bottomNavigationBar: NavigationBar(
selectedIndex: _selectedPage,
onDestinationSelected: (index) {
_pageController.animateToPage(
index,
duration: const Duration(milliseconds: 200),
@ -67,26 +59,25 @@ class _HomeState extends State<Home> {
_selectedPage = index;
});
},
items: const [
BottomNavigationBarItem(
destinations: const [
NavigationDestination(
icon: Icon(Icons.message),
label: "Chat",
),
BottomNavigationBarItem(
NavigationDestination(
icon: Icon(Icons.public),
label: "Sessions",
),
BottomNavigationBarItem(
NavigationDestination(
icon: Icon(Icons.inventory),
label: "Inventory",
),
BottomNavigationBarItem(
NavigationDestination(
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:collection/collection.dart';
import 'package:contacts_plus_plus/auxiliary.dart';
import 'package:contacts_plus_plus/clients/inventory_client.dart';
import 'package:contacts_plus_plus/models/inventory/neos_path.dart';
import 'package:contacts_plus_plus/models/records/record.dart';
import 'package:contacts_plus_plus/widgets/default_error_widget.dart';
import 'package:contacts_plus_plus/widgets/inventory/object_inventory_tile.dart';
import 'package:contacts_plus_plus/widgets/inventory/path_inventory_tile.dart';
import 'package:recon/auxiliary.dart';
import 'package:recon/clients/inventory_client.dart';
import 'package:recon/models/inventory/resonite_directory.dart';
import 'package:recon/models/records/record.dart';
import 'package:recon/widgets/default_error_widget.dart';
import 'package:recon/widgets/inventory/object_inventory_tile.dart';
import 'package:recon/widgets/inventory/path_inventory_tile.dart';
import 'package:flutter/material.dart';
import 'package:photo_view/photo_view.dart';
import 'package:provider/provider.dart';
@ -37,7 +37,7 @@ class _InventoryBrowserState extends State<InventoryBrowser> with AutomaticKeepA
Widget build(BuildContext context) {
super.build(context);
return Consumer<InventoryClient>(builder: (BuildContext context, InventoryClient iClient, Widget? child) {
return FutureBuilder<NeosDirectory>(
return FutureBuilder<ResoniteDirectory>(
future: iClient.directoryFuture,
builder: (context, snapshot) {
final currentDir = snapshot.data;
@ -57,8 +57,10 @@ class _InventoryBrowserState extends State<InventoryBrowser> with AutomaticKeepA
await iClient.reloadCurrentDirectory();
_refreshLimiter = Timer(_refreshLimit, () {});
} catch (e) {
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text("Refresh failed: $e")));
}
}
},
child: Builder(
builder: (context) {
@ -179,7 +181,7 @@ class _InventoryBrowserState extends State<InventoryBrowser> with AutomaticKeepA
builder: (context) => PhotoView(
minScale: PhotoViewComputedScale.contained,
imageProvider:
CachedNetworkImageProvider(Aux.neosDbToHttp(record.thumbnailUri)),
CachedNetworkImageProvider(Aux.resdbToHttp(record.thumbnailUri)),
heroAttributes: PhotoViewHeroAttributes(tag: record.id),
),
),

View file

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

View file

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

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