10
README.md
|
@ -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/>
|
||||
|
|
|
@ -49,7 +49,7 @@ android {
|
|||
}
|
||||
|
||||
defaultConfig {
|
||||
applicationId "me.voidspace.contacts_plus_plus"
|
||||
applicationId "me.voidspace.recon"
|
||||
minSdkVersion 21
|
||||
targetSdkVersion flutter.targetSdkVersion
|
||||
versionCode flutterVersionCode.toInteger()
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 54 KiB |
|
@ -1,4 +1,4 @@
|
|||
package de.voidspace.contacts_plus_plus
|
||||
package de.voidspace.recon
|
||||
|
||||
import io.flutter.embedding.android.FlutterActivity
|
||||
|
Before Width: | Height: | Size: 677 B After Width: | Height: | Size: 913 B |
Before Width: | Height: | Size: 461 B After Width: | Height: | Size: 542 B |
Before Width: | Height: | Size: 895 B After Width: | Height: | Size: 1.3 KiB |
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 2.2 KiB |
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 3 KiB |
|
@ -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>
|
Before Width: | Height: | Size: 2.2 KiB |
BIN
android/app/src/main/res/mipmap-hdpi/ic_launcher.webp
Normal file
After Width: | Height: | Size: 2.3 KiB |
Before Width: | Height: | Size: 2.6 KiB |
BIN
android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp
Normal file
After Width: | Height: | Size: 5.3 KiB |
Before Width: | Height: | Size: 4.1 KiB |
BIN
android/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp
Normal file
After Width: | Height: | Size: 3.9 KiB |
Before Width: | Height: | Size: 1.5 KiB |
BIN
android/app/src/main/res/mipmap-mdpi/ic_launcher.webp
Normal file
After Width: | Height: | Size: 1.5 KiB |
Before Width: | Height: | Size: 1.8 KiB |
BIN
android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp
Normal file
After Width: | Height: | Size: 3.1 KiB |
Before Width: | Height: | Size: 2.7 KiB |
BIN
android/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp
Normal file
After Width: | Height: | Size: 2.3 KiB |
Before Width: | Height: | Size: 3.1 KiB |
BIN
android/app/src/main/res/mipmap-xhdpi/ic_launcher.webp
Normal file
After Width: | Height: | Size: 3.5 KiB |
Before Width: | Height: | Size: 3.7 KiB |
After Width: | Height: | Size: 7.8 KiB |
Before Width: | Height: | Size: 5.7 KiB |
BIN
android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
Normal file
After Width: | Height: | Size: 5.6 KiB |
Before Width: | Height: | Size: 4.7 KiB |
BIN
android/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp
Normal file
After Width: | Height: | Size: 5.9 KiB |
Before Width: | Height: | Size: 5.6 KiB |
After Width: | Height: | Size: 12 KiB |
Before Width: | Height: | Size: 9 KiB |
BIN
android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
Normal file
After Width: | Height: | Size: 9.4 KiB |
Before Width: | Height: | Size: 6.4 KiB |
BIN
android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
Normal file
After Width: | Height: | Size: 6.9 KiB |
Before Width: | Height: | Size: 8.2 KiB |
After Width: | Height: | Size: 16 KiB |
Before Width: | Height: | Size: 13 KiB |
BIN
android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
Normal file
After Width: | Height: | Size: 12 KiB |
|
@ -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>
|
|
@ -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.
|
||||
|
|
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 28 KiB |
Before Width: | Height: | Size: 122 KiB After Width: | Height: | Size: 150 KiB |
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 64 KiB |
|
@ -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
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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"] ?? "";
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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";
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 ??= {};
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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,8 +80,7 @@ class MessagingClient extends ChangeNotifier {
|
|||
|
||||
MessageCache _createUserMessageCache(String userId) => MessageCache(apiClient: _apiClient, userId: userId);
|
||||
|
||||
|
||||
Future<void> refreshFriendsListWithErrorHandler () async {
|
||||
Future<void> refreshFriendsListWithErrorHandler() async {
|
||||
try {
|
||||
await refreshFriendsList();
|
||||
} catch (e) {
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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';
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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';
|
||||
|
||||
|
||||
|
|
|
@ -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
|
@ -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
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
import 'package:contacts_plus_plus/auxiliary.dart';
|
||||
import 'package:recon/auxiliary.dart';
|
||||
|
||||
class CloudVariable {
|
||||
final String ownerId;
|
||||
|
|
28
lib/models/hub_events.dart
Normal 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,
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
35
lib/models/inventory/resonite_directory.dart
Normal 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);
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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)}",
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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(),
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
import 'package:contacts_plus_plus/models/records/asset_diff.dart';
|
||||
import 'package:recon/models/records/asset_diff.dart';
|
||||
|
||||
enum RecordPreprocessState
|
||||
{
|
||||
|
|
|
@ -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(),
|
||||
|
|
26
lib/models/records/resonite_db_asset.dart
Normal 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,
|
||||
};
|
||||
}
|
||||
}
|
|
@ -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" : ""}";
|
||||
|
||||
|
|
38
lib/models/session_metadata.dart
Normal 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,
|
||||
};
|
||||
}
|
||||
}
|
|
@ -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';
|
||||
|
||||
|
|
|
@ -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(),
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
);
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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';
|
||||
|
||||
|
|
|
@ -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>(
|
||||
|
|
|
@ -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;
|
||||
});
|
||||
|
|
|
@ -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';
|
||||
|
||||
|
|
|
@ -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",
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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),
|
||||
),
|
||||
),
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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(
|
||||
|
|