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
|
## Building
|
||||||
|
|
||||||
|
@ -17,4 +17,4 @@ For example, voice-messages and notifications are currently not supported on des
|
||||||
|
|
||||||
## Screenshots
|
## Screenshots
|
||||||
|
|
||||||
<img src="https://cdn.discordapp.com/attachments/897112742035615804/1122142234905686047/Screenshot_20230624-143102_Contacts.png" width=198/> <img src="https://cdn.discordapp.com/attachments/897112742035615804/1122142235169923202/Screenshot_20230624-143035_Contacts.png" width=198/> <img src="https://cdn.discordapp.com/attachments/897112742035615804/1122142233773219890/Screenshot_20230624-143109_Contacts.png" width=198/> <img src="https://cdn.discordapp.com/attachments/897112742035615804/1122142233114726410/Screenshot_20230624-143205_Contacts.png" width=198/> <img src="https://cdn.discordapp.com/attachments/897112742035615804/1122142233458651209/Screenshot_20230624-143124_Contacts.png" width=198/>
|
<img src="https://github.com/Nutcake/ReCon/assets/10452593/a46ccf8a-0a9f-4518-98e6-84fad2d7bf26" width=198/> <img src="https://github.com/Nutcake/ReCon/assets/10452593/5d158f58-cd27-4a68-abf3-9068e92b6a82" width=198/> <img src="https://github.com/Nutcake/ReCon/assets/10452593/f2ce95ef-e513-46cb-9654-31e74cdc7c09" width=198/> <img src="https://github.com/Nutcake/ReCon/assets/10452593/58ef5e5e-1b53-4a47-92f8-bcbcba7a1e86" width=198/>
|
||||||
|
|
|
@ -49,7 +49,7 @@ android {
|
||||||
}
|
}
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId "me.voidspace.contacts_plus_plus"
|
applicationId "me.voidspace.recon"
|
||||||
minSdkVersion 21
|
minSdkVersion 21
|
||||||
targetSdkVersion flutter.targetSdkVersion
|
targetSdkVersion flutter.targetSdkVersion
|
||||||
versionCode flutterVersionCode.toInteger()
|
versionCode flutterVersionCode.toInteger()
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
package="de.voidspace.contacts_plus_plus">
|
package="de.voidspace.recon">
|
||||||
<!-- The INTERNET permission is required for development. Specifically,
|
<!-- The INTERNET permission is required for development. Specifically,
|
||||||
the Flutter tool needs it to communicate with the running application
|
the Flutter tool needs it to communicate with the running application
|
||||||
to allow setting breakpoints, to provide hot reload, etc.
|
to allow setting breakpoints, to provide hot reload, etc.
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
package="de.voidspace.contacts_plus_plus">
|
package="de.voidspace.recon">
|
||||||
|
|
||||||
<!-- Required to fetch data from the internet. -->
|
<!-- Required to fetch data from the internet. -->
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
|
@ -7,7 +7,7 @@
|
||||||
<!-- Optional, you'll have to check this permission by yourself. -->
|
<!-- Optional, you'll have to check this permission by yourself. -->
|
||||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||||
<application
|
<application
|
||||||
android:label="Contacts++"
|
android:label="ReCon"
|
||||||
android:name="${applicationName}"
|
android:name="${applicationName}"
|
||||||
android:icon="@mipmap/ic_launcher">
|
android:icon="@mipmap/ic_launcher">
|
||||||
<activity
|
<activity
|
||||||
|
|
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
|
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">
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
<background android:drawable="@color/ic_launcher_background"/>
|
<background android:drawable="@color/ic_launcher_background"/>
|
||||||
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||||
<monochrome android:drawable="@mipmap/ic_launcher_mono" />
|
|
||||||
</adaptive-icon>
|
</adaptive-icon>
|
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"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources>
|
<resources>
|
||||||
<color name="ic_launcher_background">#232426</color>
|
<color name="ic_launcher_background">#050505</color>
|
||||||
</resources>
|
</resources>
|
|
@ -1,5 +1,5 @@
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
package="de.voidspace.contacts_plus_plus">
|
package="de.voidspace.recon">
|
||||||
<!-- The INTERNET permission is required for development. Specifically,
|
<!-- The INTERNET permission is required for development. Specifically,
|
||||||
the Flutter tool needs it to communicate with the running application
|
the Flutter tool needs it to communicate with the running application
|
||||||
to allow setting breakpoints, to provide hot reload, etc.
|
to allow setting breakpoints, to provide hot reload, etc.
|
||||||
|
|
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 'dart:convert';
|
||||||
|
|
||||||
import 'package:contacts_plus_plus/clients/api_client.dart';
|
import 'package:recon/clients/api_client.dart';
|
||||||
import 'package:contacts_plus_plus/models/cloud_variable.dart';
|
import 'package:recon/models/cloud_variable.dart';
|
||||||
|
|
||||||
class CloudVariableApi {
|
class CloudVariableApi {
|
||||||
static Future<CloudVariable> readCloudVariable(ApiClient client,
|
static Future<CloudVariable> readCloudVariable(ApiClient client,
|
||||||
|
|
37
lib/apis/contact_api.dart
Normal file
|
@ -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 const baseUrl = "https://api.github.com";
|
||||||
|
|
||||||
static Future<String> getLatestTagName() async {
|
static Future<String> getLatestTagName() async {
|
||||||
final response = await http.get(Uri.parse("$baseUrl/repos/Nutcake/contacts-plus-plus/releases/latest"));
|
final response = await http.get(Uri.parse("$baseUrl/repos/Nutcake/ReCon/releases/latest"));
|
||||||
if (response.statusCode != 200) return "";
|
if (response.statusCode != 200) return "";
|
||||||
final body = jsonDecode(response.body);
|
final body = jsonDecode(response.body);
|
||||||
return body["tag_name"] ?? "";
|
return body["tag_name"] ?? "";
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
|
||||||
import 'package:contacts_plus_plus/clients/api_client.dart';
|
import 'package:recon/clients/api_client.dart';
|
||||||
import 'package:contacts_plus_plus/models/message.dart';
|
import 'package:recon/models/message.dart';
|
||||||
|
|
||||||
class MessageApi {
|
class MessageApi {
|
||||||
static Future<List<Message>> getUserMessages(ApiClient client, {String userId = "", DateTime? fromTime,
|
static Future<List<Message>> getUserMessages(ApiClient client, {String userId = "", DateTime? fromTime,
|
||||||
|
|
|
@ -3,16 +3,16 @@ import 'dart:io';
|
||||||
import 'dart:math';
|
import 'dart:math';
|
||||||
import 'dart:typed_data';
|
import 'dart:typed_data';
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
import 'package:contacts_plus_plus/models/records/asset_digest.dart';
|
import 'package:recon/models/records/asset_digest.dart';
|
||||||
import 'package:contacts_plus_plus/models/records/json_template.dart';
|
import 'package:recon/models/records/json_template.dart';
|
||||||
import 'package:http/http.dart' as http;
|
import 'package:http/http.dart' as http;
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import 'package:contacts_plus_plus/clients/api_client.dart';
|
import 'package:recon/clients/api_client.dart';
|
||||||
import 'package:contacts_plus_plus/models/records/asset_upload_data.dart';
|
import 'package:recon/models/records/asset_upload_data.dart';
|
||||||
import 'package:contacts_plus_plus/models/records/neos_db_asset.dart';
|
import 'package:recon/models/records/resonite_db_asset.dart';
|
||||||
import 'package:contacts_plus_plus/models/records/preprocess_status.dart';
|
import 'package:recon/models/records/preprocess_status.dart';
|
||||||
import 'package:contacts_plus_plus/models/records/record.dart';
|
import 'package:recon/models/records/record.dart';
|
||||||
import 'package:http_parser/http_parser.dart';
|
import 'package:http_parser/http_parser.dart';
|
||||||
import 'package:path/path.dart';
|
import 'package:path/path.dart';
|
||||||
|
|
||||||
|
@ -68,7 +68,7 @@ class RecordApi {
|
||||||
return status;
|
return status;
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<AssetUploadData> beginUploadAsset(ApiClient client, {required NeosDBAsset asset}) async {
|
static Future<AssetUploadData> beginUploadAsset(ApiClient client, {required ResoniteDBAsset asset}) async {
|
||||||
final response = await client.post("/users/${client.userId}/assets/${asset.hash}/chunks");
|
final response = await client.post("/users/${client.userId}/assets/${asset.hash}/chunks");
|
||||||
client.checkResponse(response);
|
client.checkResponse(response);
|
||||||
final body = jsonDecode(response.body);
|
final body = jsonDecode(response.body);
|
||||||
|
@ -84,7 +84,7 @@ class RecordApi {
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<void> uploadAsset(ApiClient client,
|
static Future<void> uploadAsset(ApiClient client,
|
||||||
{required AssetUploadData uploadData, required String filename, required NeosDBAsset asset, required Uint8List data, void Function(double number)? progressCallback}) async {
|
{required AssetUploadData uploadData, required String filename, required ResoniteDBAsset asset, required Uint8List data, void Function(double number)? progressCallback}) async {
|
||||||
for (int i = 0; i < uploadData.totalChunks; i++) {
|
for (int i = 0; i < uploadData.totalChunks; i++) {
|
||||||
progressCallback?.call(i/uploadData.totalChunks);
|
progressCallback?.call(i/uploadData.totalChunks);
|
||||||
final offset = i * uploadData.chunkSize;
|
final offset = i * uploadData.chunkSize;
|
||||||
|
@ -104,7 +104,7 @@ class RecordApi {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<void> finishUpload(ApiClient client, {required NeosDBAsset asset}) async {
|
static Future<void> finishUpload(ApiClient client, {required ResoniteDBAsset asset}) async {
|
||||||
final response = await client.patch("/users/${client.userId}/assets/${asset.hash}/chunks");
|
final response = await client.patch("/users/${client.userId}/assets/${asset.hash}/chunks");
|
||||||
client.checkResponse(response);
|
client.checkResponse(response);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
|
||||||
import 'package:contacts_plus_plus/clients/api_client.dart';
|
import 'package:recon/clients/api_client.dart';
|
||||||
import 'package:contacts_plus_plus/models/session.dart';
|
import 'package:recon/models/session.dart';
|
||||||
|
|
||||||
class SessionApi {
|
class SessionApi {
|
||||||
static Future<Session> getSession(ApiClient client, {required String sessionId}) async {
|
static Future<Session> getSession(ApiClient client, {required String sessionId}) async {
|
||||||
|
|
|
@ -1,13 +1,9 @@
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
|
||||||
import 'package:contacts_plus_plus/clients/api_client.dart';
|
import 'package:recon/clients/api_client.dart';
|
||||||
import 'package:contacts_plus_plus/models/users/friend.dart';
|
import 'package:recon/models/personal_profile.dart';
|
||||||
import 'package:contacts_plus_plus/models/personal_profile.dart';
|
import 'package:recon/models/users/user.dart';
|
||||||
import 'package:contacts_plus_plus/models/users/user.dart';
|
import 'package:recon/models/users/user_status.dart';
|
||||||
import 'package:contacts_plus_plus/models/users/user_profile.dart';
|
|
||||||
import 'package:contacts_plus_plus/models/users/friend_status.dart';
|
|
||||||
import 'package:contacts_plus_plus/models/users/user_status.dart';
|
|
||||||
import 'package:package_info_plus/package_info_plus.dart';
|
|
||||||
|
|
||||||
class UserApi {
|
class UserApi {
|
||||||
static Future<Iterable<User>> searchUsers(ApiClient client, {required String needle}) async {
|
static Future<Iterable<User>> searchUsers(ApiClient client, {required String needle}) async {
|
||||||
|
@ -25,6 +21,7 @@ class UserApi {
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<UserStatus> getUserStatus(ApiClient client, {required String userId}) async {
|
static Future<UserStatus> getUserStatus(ApiClient client, {required String userId}) async {
|
||||||
|
return UserStatus.empty();
|
||||||
final response = await client.get("/users/$userId/status");
|
final response = await client.get("/users/$userId/status");
|
||||||
client.checkResponse(response);
|
client.checkResponse(response);
|
||||||
final data = jsonDecode(response.body);
|
final data = jsonDecode(response.body);
|
||||||
|
@ -32,18 +29,7 @@ class UserApi {
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<void> notifyOnlineInstance(ApiClient client) async {
|
static Future<void> notifyOnlineInstance(ApiClient client) async {
|
||||||
final response = await client.post("/stats/instanceOnline/${client.authenticationData.secretMachineId.hashCode}");
|
final response = await client.post("/stats/instanceOnline/${client.authenticationData.secretMachineIdHash}");
|
||||||
client.checkResponse(response);
|
|
||||||
}
|
|
||||||
|
|
||||||
static Future<void> setStatus(ApiClient client, {required UserStatus status}) async {
|
|
||||||
final pkginfo = await PackageInfo.fromPlatform();
|
|
||||||
status = status.copyWith(
|
|
||||||
neosVersion: "${pkginfo.version} of ${pkginfo.appName}",
|
|
||||||
isMobile: true,
|
|
||||||
);
|
|
||||||
final body = jsonEncode(status.toMap(shallow: true));
|
|
||||||
final response = await client.put("/users/${client.userId}/status", body: body);
|
|
||||||
client.checkResponse(response);
|
client.checkResponse(response);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -53,24 +39,4 @@ class UserApi {
|
||||||
final data = jsonDecode(response.body);
|
final data = jsonDecode(response.body);
|
||||||
return PersonalProfile.fromMap(data);
|
return PersonalProfile.fromMap(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<void> addUserAsFriend(ApiClient client, {required User user}) async {
|
|
||||||
final friend = Friend(
|
|
||||||
id: user.id,
|
|
||||||
username: user.username,
|
|
||||||
ownerId: client.userId,
|
|
||||||
userStatus: UserStatus.empty(),
|
|
||||||
userProfile: UserProfile.empty(),
|
|
||||||
friendStatus: FriendStatus.accepted,
|
|
||||||
latestMessageTime: DateTime.now(),
|
|
||||||
);
|
|
||||||
final body = jsonEncode(friend.toMap(shallow: true));
|
|
||||||
final response = await client.put("/users/${client.userId}/friends/${user.id}", body: body);
|
|
||||||
client.checkResponse(response);
|
|
||||||
}
|
|
||||||
|
|
||||||
static Future<void> removeUserAsFriend(ApiClient client, {required User user}) async {
|
|
||||||
final response = await client.delete("/users/${client.userId}/friends/${user.id}");
|
|
||||||
client.checkResponse(response);
|
|
||||||
}
|
|
||||||
}
|
}
|
|
@ -1,55 +1,14 @@
|
||||||
import 'package:contacts_plus_plus/config.dart';
|
import 'package:recon/config.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:path/path.dart' as p;
|
import 'package:path/path.dart' as p;
|
||||||
import 'package:html/parser.dart' as htmlparser;
|
import 'package:html/parser.dart' as htmlparser;
|
||||||
|
|
||||||
enum NeosDBEndpoint
|
|
||||||
{
|
|
||||||
def,
|
|
||||||
blob,
|
|
||||||
cdn,
|
|
||||||
videoCDN,
|
|
||||||
}
|
|
||||||
|
|
||||||
extension NeosStringExtensions on Uri {
|
|
||||||
static String dbSignature(Uri neosdb) => neosdb.pathSegments.length < 2 ? "" : p.basenameWithoutExtension(neosdb.pathSegments[1]);
|
|
||||||
static String? neosDBQuery(Uri neosdb) => neosdb.query.trim().isEmpty ? null : neosdb.query.substring(1);
|
|
||||||
static bool isLegacyNeosDB(Uri uri) => !(uri.scheme != "neosdb") && uri.pathSegments.length >= 2 && p.basenameWithoutExtension(uri.pathSegments[1]).length < 30;
|
|
||||||
|
|
||||||
Uri neosDBToHTTP(NeosDBEndpoint endpoint) {
|
|
||||||
var signature = dbSignature(this);
|
|
||||||
var query = neosDBQuery(this);
|
|
||||||
if (query != null) {
|
|
||||||
signature = "$signature/$query";
|
|
||||||
}
|
|
||||||
if (isLegacyNeosDB(this)) {
|
|
||||||
return Uri.parse(Config.legacyCloudUrl + signature);
|
|
||||||
}
|
|
||||||
String base;
|
|
||||||
switch (endpoint) {
|
|
||||||
case NeosDBEndpoint.blob:
|
|
||||||
base = Config.blobStorageUrl;
|
|
||||||
break;
|
|
||||||
case NeosDBEndpoint.cdn:
|
|
||||||
base = Config.neosCdnUrl;
|
|
||||||
break;
|
|
||||||
case NeosDBEndpoint.videoCDN:
|
|
||||||
base = Config.videoStorageUrl;
|
|
||||||
break;
|
|
||||||
case NeosDBEndpoint.def:
|
|
||||||
base = Config.neosAssetsUrl;
|
|
||||||
}
|
|
||||||
|
|
||||||
return Uri.parse(base + signature);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class Aux {
|
class Aux {
|
||||||
static String neosDbToHttp(String? neosdb) {
|
static String resdbToHttp(String? resdb) {
|
||||||
if (neosdb == null || neosdb.isEmpty) return "";
|
if (resdb == null || resdb.isEmpty) return "";
|
||||||
if (neosdb.startsWith("http")) return neosdb;
|
if (resdb.startsWith("http")) return resdb;
|
||||||
final filename = p.basenameWithoutExtension(neosdb);
|
final filename = p.basenameWithoutExtension(resdb);
|
||||||
return "${Config.neosCdnUrl}$filename";
|
return "${Config.skyfrostAssetsUrl}/$filename";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
|
|
||||||
import 'package:contacts_plus_plus/clients/api_client.dart';
|
import 'package:recon/clients/api_client.dart';
|
||||||
import 'package:contacts_plus_plus/clients/notification_client.dart';
|
import 'package:recon/clients/notification_client.dart';
|
||||||
import 'package:contacts_plus_plus/clients/settings_client.dart';
|
import 'package:recon/clients/settings_client.dart';
|
||||||
import 'package:contacts_plus_plus/models/authentication_data.dart';
|
import 'package:recon/models/authentication_data.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
class ClientHolder extends InheritedWidget {
|
class ClientHolder extends InheritedWidget {
|
||||||
|
|
|
@ -3,7 +3,7 @@ import 'dart:convert';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||||
import 'package:http/http.dart' as http;
|
import 'package:http/http.dart' as http;
|
||||||
import 'package:contacts_plus_plus/models/authentication_data.dart';
|
import 'package:recon/models/authentication_data.dart';
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
import 'package:uuid/uuid.dart';
|
import 'package:uuid/uuid.dart';
|
||||||
|
|
||||||
|
@ -15,6 +15,7 @@ class ApiClient {
|
||||||
static const String machineIdKey = "machineId";
|
static const String machineIdKey = "machineId";
|
||||||
static const String tokenKey = "token";
|
static const String tokenKey = "token";
|
||||||
static const String passwordKey = "password";
|
static const String passwordKey = "password";
|
||||||
|
static const String uidKey = "uid";
|
||||||
|
|
||||||
ApiClient({required AuthenticationData authenticationData, required this.onLogout})
|
ApiClient({required AuthenticationData authenticationData, required this.onLogout})
|
||||||
: _authenticationData = authenticationData;
|
: _authenticationData = authenticationData;
|
||||||
|
@ -41,14 +42,19 @@ class ApiClient {
|
||||||
}) async {
|
}) async {
|
||||||
final body = {
|
final body = {
|
||||||
(username.contains("@") ? "email" : "username"): username.trim(),
|
(username.contains("@") ? "email" : "username"): username.trim(),
|
||||||
"password": password,
|
"authentication": {
|
||||||
|
"\$type": "password",
|
||||||
|
"password": password,
|
||||||
|
},
|
||||||
"rememberMe": rememberMe,
|
"rememberMe": rememberMe,
|
||||||
"secretMachineId": const Uuid().v4(),
|
"secretMachineId": const Uuid().v4(),
|
||||||
};
|
};
|
||||||
|
final uid = const Uuid().v4().replaceAll("-", "");
|
||||||
final response = await http.post(
|
final response = await http.post(
|
||||||
buildFullUri("/UserSessions"),
|
buildFullUri("/userSessions"),
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
|
"UID": uid,
|
||||||
if (oneTimePad != null) totpKey: oneTimePad,
|
if (oneTimePad != null) totpKey: oneTimePad,
|
||||||
},
|
},
|
||||||
body: jsonEncode(body),
|
body: jsonEncode(body),
|
||||||
|
@ -60,15 +66,17 @@ class ApiClient {
|
||||||
throw "Invalid Credentials";
|
throw "Invalid Credentials";
|
||||||
}
|
}
|
||||||
checkResponseCode(response);
|
checkResponseCode(response);
|
||||||
|
final data = jsonDecode(response.body);
|
||||||
final authData = AuthenticationData.fromMap(jsonDecode(response.body));
|
data["entity"]["uid"] = uid;
|
||||||
|
final authData = AuthenticationData.fromMap(data);
|
||||||
if (authData.isAuthenticated) {
|
if (authData.isAuthenticated) {
|
||||||
const FlutterSecureStorage storage = FlutterSecureStorage(
|
const FlutterSecureStorage storage = FlutterSecureStorage(
|
||||||
aOptions: AndroidOptions(encryptedSharedPreferences: true),
|
aOptions: AndroidOptions(encryptedSharedPreferences: true),
|
||||||
);
|
);
|
||||||
await storage.write(key: userIdKey, value: authData.userId);
|
await storage.write(key: userIdKey, value: authData.userId);
|
||||||
await storage.write(key: machineIdKey, value: authData.secretMachineId);
|
await storage.write(key: machineIdKey, value: authData.secretMachineIdHash);
|
||||||
await storage.write(key: tokenKey, value: authData.token);
|
await storage.write(key: tokenKey, value: authData.token);
|
||||||
|
await storage.write(key: uidKey, value: authData.uid);
|
||||||
if (rememberPass) await storage.write(key: passwordKey, value: password);
|
if (rememberPass) await storage.write(key: passwordKey, value: password);
|
||||||
}
|
}
|
||||||
return authData;
|
return authData;
|
||||||
|
@ -82,16 +90,25 @@ class ApiClient {
|
||||||
String? machineId = await storage.read(key: machineIdKey);
|
String? machineId = await storage.read(key: machineIdKey);
|
||||||
String? token = await storage.read(key: tokenKey);
|
String? token = await storage.read(key: tokenKey);
|
||||||
String? password = await storage.read(key: passwordKey);
|
String? password = await storage.read(key: passwordKey);
|
||||||
|
String? uid = await storage.read(key: uidKey);
|
||||||
|
|
||||||
if (userId == null || machineId == null) {
|
if (userId == null || machineId == null || uid == null) {
|
||||||
return AuthenticationData.unauthenticated();
|
return AuthenticationData.unauthenticated();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (token != null) {
|
if (token != null) {
|
||||||
final response =
|
final response = await http.patch(buildFullUri("/userSessions"), headers: {
|
||||||
await http.patch(buildFullUri("/userSessions"), headers: {"Authorization": "neos $userId:$token"});
|
"Authorization": "res $userId:$token",
|
||||||
|
"UID": uid,
|
||||||
|
});
|
||||||
if (response.statusCode < 300) {
|
if (response.statusCode < 300) {
|
||||||
return AuthenticationData(userId: userId, token: token, secretMachineId: machineId, isAuthenticated: true);
|
return AuthenticationData(
|
||||||
|
userId: userId,
|
||||||
|
token: token,
|
||||||
|
secretMachineIdHash: machineId,
|
||||||
|
isAuthenticated: true,
|
||||||
|
uid: uid,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -148,13 +165,16 @@ class ApiClient {
|
||||||
_ => "Unknown Error."
|
_ => "Unknown Error."
|
||||||
}} (${response.statusCode}${kDebugMode && response.body.isNotEmpty ? "|${response.body}" : ""})";
|
}} (${response.statusCode}${kDebugMode && response.body.isNotEmpty ? "|${response.body}" : ""})";
|
||||||
|
|
||||||
FlutterError.reportError(FlutterErrorDetails(exception: error));
|
FlutterError.reportError(FlutterErrorDetails(
|
||||||
|
exception: error,
|
||||||
|
stack: StackTrace.current,
|
||||||
|
));
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
||||||
Map<String, String> get authorizationHeader => _authenticationData.authorizationHeader;
|
Map<String, String> get authorizationHeader => _authenticationData.authorizationHeader;
|
||||||
|
|
||||||
static Uri buildFullUri(String path) => Uri.parse("${Config.apiBaseUrl}/api$path");
|
static Uri buildFullUri(String path) => Uri.parse("${Config.apiBaseUrl}$path");
|
||||||
|
|
||||||
Future<http.Response> get(String path, {Map<String, String>? headers}) async {
|
Future<http.Response> get(String path, {Map<String, String>? headers}) async {
|
||||||
headers ??= {};
|
headers ??= {};
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:contacts_plus_plus/auxiliary.dart';
|
import 'package:recon/auxiliary.dart';
|
||||||
import 'package:contacts_plus_plus/clients/api_client.dart';
|
import 'package:recon/clients/api_client.dart';
|
||||||
import 'package:http/http.dart' as http;
|
import 'package:http/http.dart' as http;
|
||||||
import 'package:contacts_plus_plus/models/message.dart';
|
import 'package:recon/models/message.dart';
|
||||||
import 'package:path/path.dart';
|
import 'package:path/path.dart';
|
||||||
import 'package:path_provider/path_provider.dart';
|
import 'package:path_provider/path_provider.dart';
|
||||||
|
|
||||||
|
@ -15,7 +15,7 @@ class AudioCacheClient {
|
||||||
final file = File("${directory.path}/${basename(clip.assetUri)}");
|
final file = File("${directory.path}/${basename(clip.assetUri)}");
|
||||||
if (!await file.exists()) {
|
if (!await file.exists()) {
|
||||||
await file.create(recursive: true);
|
await file.create(recursive: true);
|
||||||
final response = await http.get(Uri.parse(Aux.neosDbToHttp(clip.assetUri)));
|
final response = await http.get(Uri.parse(Aux.resdbToHttp(clip.assetUri)));
|
||||||
ApiClient.checkResponseCode(response);
|
ApiClient.checkResponseCode(response);
|
||||||
await file.writeAsBytes(response.bodyBytes);
|
await file.writeAsBytes(response.bodyBytes);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,17 +1,17 @@
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:contacts_plus_plus/apis/record_api.dart';
|
import 'package:recon/apis/record_api.dart';
|
||||||
import 'package:contacts_plus_plus/clients/api_client.dart';
|
import 'package:recon/clients/api_client.dart';
|
||||||
import 'package:contacts_plus_plus/models/inventory/neos_path.dart';
|
import 'package:recon/models/inventory/resonite_directory.dart';
|
||||||
import 'package:contacts_plus_plus/models/records/record.dart';
|
import 'package:recon/models/records/record.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
class InventoryClient extends ChangeNotifier {
|
class InventoryClient extends ChangeNotifier {
|
||||||
final ApiClient apiClient;
|
final ApiClient apiClient;
|
||||||
|
|
||||||
Future<NeosDirectory>? _currentDirectory;
|
Future<ResoniteDirectory>? _currentDirectory;
|
||||||
|
|
||||||
Future<NeosDirectory>? get directoryFuture => _currentDirectory;
|
Future<ResoniteDirectory>? get directoryFuture => _currentDirectory;
|
||||||
|
|
||||||
InventoryClient({required this.apiClient});
|
InventoryClient({required this.apiClient});
|
||||||
|
|
||||||
|
@ -51,7 +51,7 @@ class InventoryClient extends ChangeNotifier {
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<List<Record>> _getDirectory(Record record) async {
|
Future<List<Record>> _getDirectory(Record record) async {
|
||||||
NeosDirectory? dir;
|
ResoniteDirectory? dir;
|
||||||
try {
|
try {
|
||||||
dir = await _currentDirectory;
|
dir = await _currentDirectory;
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
|
@ -59,7 +59,7 @@ class InventoryClient extends ChangeNotifier {
|
||||||
if (dir == null || record.isRoot) {
|
if (dir == null || record.isRoot) {
|
||||||
records = await RecordApi.getUserRecordsAt(
|
records = await RecordApi.getUserRecordsAt(
|
||||||
apiClient,
|
apiClient,
|
||||||
path: NeosDirectory.rootName,
|
path: ResoniteDirectory.rootName,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
if (record.recordType == RecordType.link) {
|
if (record.recordType == RecordType.link) {
|
||||||
|
@ -79,12 +79,12 @@ class InventoryClient extends ChangeNotifier {
|
||||||
final rootRecord = Record.inventoryRoot();
|
final rootRecord = Record.inventoryRoot();
|
||||||
final rootFuture = _getDirectory(rootRecord).then(
|
final rootFuture = _getDirectory(rootRecord).then(
|
||||||
(records) {
|
(records) {
|
||||||
final rootDir = NeosDirectory(
|
final rootDir = ResoniteDirectory(
|
||||||
record: rootRecord,
|
record: rootRecord,
|
||||||
children: [],
|
children: [],
|
||||||
);
|
);
|
||||||
rootDir.children.addAll(
|
rootDir.children.addAll(
|
||||||
records.map((e) => NeosDirectory.fromRecord(record: e, parent: rootDir)).toList(),
|
records.map((e) => ResoniteDirectory.fromRecord(record: e, parent: rootDir)).toList(),
|
||||||
);
|
);
|
||||||
return rootDir;
|
return rootDir;
|
||||||
},
|
},
|
||||||
|
@ -103,8 +103,8 @@ class InventoryClient extends ChangeNotifier {
|
||||||
|
|
||||||
_currentDirectory = _getDirectory(dir.record).then(
|
_currentDirectory = _getDirectory(dir.record).then(
|
||||||
(records) {
|
(records) {
|
||||||
final children = records.map((record) => NeosDirectory.fromRecord(record: record, parent: dir)).toList();
|
final children = records.map((record) => ResoniteDirectory.fromRecord(record: record, parent: dir)).toList();
|
||||||
final newDir = NeosDirectory(record: dir.record, children: children, parent: dir.parent);
|
final newDir = ResoniteDirectory(record: dir.record, children: children, parent: dir.parent);
|
||||||
|
|
||||||
final parentIdx = dir.parent?.children.indexOf(dir) ?? -1;
|
final parentIdx = dir.parent?.children.indexOf(dir) ?? -1;
|
||||||
if (parentIdx != -1) {
|
if (parentIdx != -1) {
|
||||||
|
@ -142,7 +142,7 @@ class InventoryClient extends ChangeNotifier {
|
||||||
_currentDirectory = _getDirectory(record).then(
|
_currentDirectory = _getDirectory(record).then(
|
||||||
(records) {
|
(records) {
|
||||||
childDir.children.clear();
|
childDir.children.clear();
|
||||||
childDir.children.addAll(records.map((record) => NeosDirectory.fromRecord(record: record, parent: childDir)));
|
childDir.children.addAll(records.map((record) => ResoniteDirectory.fromRecord(record: record, parent: childDir)));
|
||||||
return childDir;
|
return childDir;
|
||||||
},
|
},
|
||||||
).onError((error, stackTrace) {
|
).onError((error, stackTrace) {
|
||||||
|
|
|
@ -1,50 +1,25 @@
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:convert';
|
|
||||||
import 'dart:io';
|
|
||||||
|
|
||||||
|
import 'package:recon/apis/contact_api.dart';
|
||||||
|
import 'package:recon/apis/message_api.dart';
|
||||||
|
import 'package:recon/apis/user_api.dart';
|
||||||
|
import 'package:recon/clients/api_client.dart';
|
||||||
|
import 'package:recon/clients/notification_client.dart';
|
||||||
|
import 'package:recon/crypto_helper.dart';
|
||||||
|
import 'package:recon/hub_manager.dart';
|
||||||
|
import 'package:recon/models/hub_events.dart';
|
||||||
|
import 'package:recon/models/message.dart';
|
||||||
|
import 'package:recon/models/session.dart';
|
||||||
|
import 'package:recon/models/users/friend.dart';
|
||||||
|
import 'package:recon/models/users/user_status.dart';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
import 'package:hive_flutter/hive_flutter.dart';
|
import 'package:hive_flutter/hive_flutter.dart';
|
||||||
import 'package:http/http.dart' as http;
|
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
|
import 'package:package_info_plus/package_info_plus.dart';
|
||||||
|
|
||||||
import 'package:contacts_plus_plus/apis/friend_api.dart';
|
|
||||||
import 'package:contacts_plus_plus/apis/message_api.dart';
|
|
||||||
import 'package:contacts_plus_plus/apis/user_api.dart';
|
|
||||||
import 'package:contacts_plus_plus/clients/notification_client.dart';
|
|
||||||
import 'package:contacts_plus_plus/models/users/friend.dart';
|
|
||||||
import 'package:contacts_plus_plus/clients/api_client.dart';
|
|
||||||
import 'package:contacts_plus_plus/config.dart';
|
|
||||||
import 'package:contacts_plus_plus/models/message.dart';
|
|
||||||
|
|
||||||
enum EventType {
|
|
||||||
unknown,
|
|
||||||
message,
|
|
||||||
unknown1,
|
|
||||||
unknown2,
|
|
||||||
unknown3,
|
|
||||||
unknown4,
|
|
||||||
keepAlive,
|
|
||||||
error;
|
|
||||||
}
|
|
||||||
|
|
||||||
enum EventTarget {
|
|
||||||
unknown,
|
|
||||||
messageSent,
|
|
||||||
receiveMessage,
|
|
||||||
messagesRead;
|
|
||||||
|
|
||||||
factory EventTarget.parse(String? text) {
|
|
||||||
if (text == null) return EventTarget.unknown;
|
|
||||||
return EventTarget.values.firstWhere((element) => element.name.toLowerCase() == text.toLowerCase(),
|
|
||||||
orElse: () => EventTarget.unknown,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class MessagingClient extends ChangeNotifier {
|
class MessagingClient extends ChangeNotifier {
|
||||||
static const String _eofChar = "";
|
|
||||||
static const String _negotiationPacket = "{\"protocol\":\"json\", \"version\":1}$_eofChar";
|
|
||||||
static const List<int> _reconnectTimeoutsSeconds = [0, 5, 10, 20, 60];
|
|
||||||
static const Duration _autoRefreshDuration = Duration(seconds: 10);
|
static const Duration _autoRefreshDuration = Duration(seconds: 10);
|
||||||
static const Duration _unreadSafeguardDuration = Duration(seconds: 120);
|
static const Duration _unreadSafeguardDuration = Duration(seconds: 120);
|
||||||
static const String _messageBoxKey = "message-box";
|
static const String _messageBoxKey = "message-box";
|
||||||
|
@ -54,32 +29,27 @@ class MessagingClient extends ChangeNotifier {
|
||||||
final List<Friend> _sortedFriendsCache = []; // Keep a sorted copy so as to not have to sort during build()
|
final List<Friend> _sortedFriendsCache = []; // Keep a sorted copy so as to not have to sort during build()
|
||||||
final Map<String, MessageCache> _messageCache = {};
|
final Map<String, MessageCache> _messageCache = {};
|
||||||
final Map<String, List<Message>> _unreads = {};
|
final Map<String, List<Message>> _unreads = {};
|
||||||
final Logger _logger = Logger("NeosHub");
|
final Logger _logger = Logger("Messaging");
|
||||||
final NotificationClient _notificationClient;
|
final NotificationClient _notificationClient;
|
||||||
|
final HubManager _hubManager = HubManager();
|
||||||
|
final Map<String, Session> _sessionMap = {};
|
||||||
Friend? selectedFriend;
|
Friend? selectedFriend;
|
||||||
|
|
||||||
Timer? _notifyOnlineTimer;
|
Timer? _notifyOnlineTimer;
|
||||||
Timer? _autoRefresh;
|
Timer? _autoRefresh;
|
||||||
Timer? _unreadSafeguard;
|
Timer? _unreadSafeguard;
|
||||||
int _attempts = 0;
|
|
||||||
WebSocket? _wsChannel;
|
|
||||||
bool _isConnecting = false;
|
|
||||||
String? _initStatus;
|
String? _initStatus;
|
||||||
|
UserStatus _userStatus = UserStatus.initial();
|
||||||
|
|
||||||
|
UserStatus get userStatus => _userStatus;
|
||||||
|
|
||||||
MessagingClient({required ApiClient apiClient, required NotificationClient notificationClient})
|
MessagingClient({required ApiClient apiClient, required NotificationClient notificationClient})
|
||||||
: _apiClient = apiClient, _notificationClient = notificationClient {
|
: _apiClient = apiClient,
|
||||||
|
_notificationClient = notificationClient {
|
||||||
debugPrint("mClient created: $hashCode");
|
debugPrint("mClient created: $hashCode");
|
||||||
Hive.openBox(_messageBoxKey).then((box) async {
|
Hive.openBox(_messageBoxKey).then((box) async {
|
||||||
box.delete(_lastUpdateKey);
|
await box.delete(_lastUpdateKey);
|
||||||
await refreshFriendsListWithErrorHandler();
|
_setupHub();
|
||||||
await _refreshUnreads();
|
|
||||||
_unreadSafeguard = Timer.periodic(_unreadSafeguardDuration, (timer) => _refreshUnreads());
|
|
||||||
});
|
|
||||||
_startWebsocket();
|
|
||||||
_notifyOnlineTimer = Timer.periodic(const Duration(seconds: 60), (timer) async {
|
|
||||||
// We should probably let the MessagingClient handle the entire state of USerStatus instead of mirroring like this
|
|
||||||
// but I don't feel like implementing that right now.
|
|
||||||
UserApi.setStatus(apiClient, status: await UserApi.getUserStatus(apiClient, userId: apiClient.userId));
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -89,21 +59,20 @@ class MessagingClient extends ChangeNotifier {
|
||||||
_autoRefresh?.cancel();
|
_autoRefresh?.cancel();
|
||||||
_notifyOnlineTimer?.cancel();
|
_notifyOnlineTimer?.cancel();
|
||||||
_unreadSafeguard?.cancel();
|
_unreadSafeguard?.cancel();
|
||||||
_wsChannel?.close();
|
_hubManager.dispose();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
String? get initStatus => _initStatus;
|
String? get initStatus => _initStatus;
|
||||||
|
|
||||||
bool get websocketConnected => _wsChannel != null;
|
|
||||||
|
|
||||||
List<Friend> get cachedFriends => _sortedFriendsCache;
|
List<Friend> get cachedFriends => _sortedFriendsCache;
|
||||||
|
|
||||||
List<Message> getUnreadsForFriend(Friend friend) => _unreads[friend.id] ?? [];
|
List<Message> getUnreadsForFriend(Friend friend) => _unreads[friend.id] ?? [];
|
||||||
|
|
||||||
bool friendHasUnreads(Friend friend) => _unreads.containsKey(friend.id);
|
bool friendHasUnreads(Friend friend) => _unreads.containsKey(friend.id);
|
||||||
|
|
||||||
bool messageIsUnread(Message message) => _unreads[message.senderId]?.any((element) => element.id == message.id) ?? false;
|
bool messageIsUnread(Message message) =>
|
||||||
|
_unreads[message.senderId]?.any((element) => element.id == message.id) ?? false;
|
||||||
|
|
||||||
Friend? getAsFriend(String userId) => Friend.fromMapOrNull(Hive.box(_messageBoxKey).get(userId));
|
Friend? getAsFriend(String userId) => Friend.fromMapOrNull(Hive.box(_messageBoxKey).get(userId));
|
||||||
|
|
||||||
|
@ -111,8 +80,7 @@ class MessagingClient extends ChangeNotifier {
|
||||||
|
|
||||||
MessageCache _createUserMessageCache(String userId) => MessageCache(apiClient: _apiClient, userId: userId);
|
MessageCache _createUserMessageCache(String userId) => MessageCache(apiClient: _apiClient, userId: userId);
|
||||||
|
|
||||||
|
Future<void> refreshFriendsListWithErrorHandler() async {
|
||||||
Future<void> refreshFriendsListWithErrorHandler () async {
|
|
||||||
try {
|
try {
|
||||||
await refreshFriendsList();
|
await refreshFriendsList();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
@ -126,25 +94,18 @@ class MessagingClient extends ChangeNotifier {
|
||||||
_autoRefresh?.cancel();
|
_autoRefresh?.cancel();
|
||||||
_autoRefresh = Timer(_autoRefreshDuration, () => refreshFriendsList());
|
_autoRefresh = Timer(_autoRefreshDuration, () => refreshFriendsList());
|
||||||
|
|
||||||
final friends = await FriendApi.getFriendsList(_apiClient, lastStatusUpdate: lastUpdateUtc);
|
final friends = await ContactApi.getFriendsList(_apiClient, lastStatusUpdate: lastUpdateUtc);
|
||||||
for (final friend in friends) {
|
for (final friend in friends) {
|
||||||
await _updateFriend(friend);
|
await _updateContact(friend);
|
||||||
}
|
}
|
||||||
|
|
||||||
_initStatus = "";
|
_initStatus = "";
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
void sendMessage(Message message) async {
|
void sendMessage(Message message) {
|
||||||
final msgBody = message.toMap();
|
final msgBody = message.toMap();
|
||||||
final data = {
|
_hubManager.send("SendMessage", arguments: [msgBody]);
|
||||||
"type": EventType.message.index,
|
|
||||||
"target": "SendMessage",
|
|
||||||
"arguments": [
|
|
||||||
msgBody
|
|
||||||
],
|
|
||||||
};
|
|
||||||
_sendData(data);
|
|
||||||
final cache = getUserMessageCache(message.recipientId) ?? _createUserMessageCache(message.recipientId);
|
final cache = getUserMessageCache(message.recipientId) ?? _createUserMessageCache(message.recipientId);
|
||||||
cache.addMessage(message);
|
cache.addMessage(message);
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
|
@ -152,17 +113,37 @@ class MessagingClient extends ChangeNotifier {
|
||||||
|
|
||||||
void markMessagesRead(MarkReadBatch batch) {
|
void markMessagesRead(MarkReadBatch batch) {
|
||||||
final msgBody = batch.toMap();
|
final msgBody = batch.toMap();
|
||||||
final data = {
|
_hubManager.send("MarkMessagesRead", arguments: [msgBody]);
|
||||||
"type": EventType.message.index,
|
|
||||||
"target": "MarkMessagesRead",
|
|
||||||
"arguments": [
|
|
||||||
msgBody
|
|
||||||
],
|
|
||||||
};
|
|
||||||
_sendData(data);
|
|
||||||
clearUnreadsForUser(batch.senderId);
|
clearUnreadsForUser(batch.senderId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> setUserStatus(UserStatus status) async {
|
||||||
|
final pkginfo = await PackageInfo.fromPlatform();
|
||||||
|
|
||||||
|
_userStatus = _userStatus.copyWith(
|
||||||
|
appVersion: "${pkginfo.version} of ${pkginfo.appName}",
|
||||||
|
lastStatusChange: DateTime.now(),
|
||||||
|
onlineStatus: status.onlineStatus,
|
||||||
|
);
|
||||||
|
|
||||||
|
_hubManager.send(
|
||||||
|
"BroadcastStatus",
|
||||||
|
arguments: [
|
||||||
|
_userStatus.toMap(),
|
||||||
|
{
|
||||||
|
"group": 1,
|
||||||
|
"targetIds": null,
|
||||||
|
}
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
final self = getAsFriend(_apiClient.userId);
|
||||||
|
if (self != null) {
|
||||||
|
await _updateContact(self.copyWith(userStatus: _userStatus));
|
||||||
|
}
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
void addUnread(Message message) {
|
void addUnread(Message message) {
|
||||||
var messages = _unreads[message.senderId];
|
var messages = _unreads[message.senderId];
|
||||||
if (messages == null) {
|
if (messages == null) {
|
||||||
|
@ -211,7 +192,7 @@ class MessagingClient extends ChangeNotifier {
|
||||||
final friend = getAsFriend(userId);
|
final friend = getAsFriend(userId);
|
||||||
if (friend == null) return;
|
if (friend == null) return;
|
||||||
final newStatus = await UserApi.getUserStatus(_apiClient, userId: userId);
|
final newStatus = await UserApi.getUserStatus(_apiClient, userId: userId);
|
||||||
await _updateFriend(friend.copyWith(userStatus: newStatus));
|
await _updateContact(friend.copyWith(userStatus: newStatus));
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -238,7 +219,7 @@ class MessagingClient extends ChangeNotifier {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _updateFriend(Friend friend) async {
|
Future<void> _updateContact(Friend friend) async {
|
||||||
final box = Hive.box(_messageBoxKey);
|
final box = Hive.box(_messageBoxKey);
|
||||||
box.put(friend.id, friend.toMap());
|
box.put(friend.id, friend.toMap());
|
||||||
final lastStatusUpdate = box.get(_lastUpdateKey);
|
final lastStatusUpdate = box.get(_lastUpdateKey);
|
||||||
|
@ -257,136 +238,100 @@ class MessagingClient extends ChangeNotifier {
|
||||||
_sortFriendsCache();
|
_sortFriendsCache();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===== Websocket Stuff =====
|
Future<void> _setupHub() async {
|
||||||
|
|
||||||
void _onDisconnected(error) async {
|
|
||||||
_wsChannel = null;
|
|
||||||
_logger.warning("Neos Hub connection died with error '$error', reconnecting...");
|
|
||||||
await _startWebsocket();
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _startWebsocket() async {
|
|
||||||
if (!_apiClient.isAuthenticated) {
|
if (!_apiClient.isAuthenticated) {
|
||||||
_logger.info("Tried to connect to Neos Hub without authentication, this is probably fine for now.");
|
_logger.info("Tried to connect to Resonite Hub without authentication, this is probably fine for now.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (_isConnecting) {
|
_hubManager.setHeaders(_apiClient.authorizationHeader);
|
||||||
return;
|
|
||||||
}
|
|
||||||
_isConnecting = true;
|
|
||||||
_wsChannel = await _tryConnect();
|
|
||||||
_isConnecting = false;
|
|
||||||
_logger.info("Connected to Neos Hub.");
|
|
||||||
_wsChannel!.done.then((error) => _onDisconnected(error));
|
|
||||||
_wsChannel!.listen(_handleEvent, onDone: () => _onDisconnected("Connection closed."), onError: _onDisconnected);
|
|
||||||
_wsChannel!.add(_negotiationPacket);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<WebSocket> _tryConnect() async {
|
_hubManager.setHandler(EventTarget.messageSent, _onMessageSent);
|
||||||
while (true) {
|
_hubManager.setHandler(EventTarget.receiveMessage, _onReceiveMessage);
|
||||||
try {
|
_hubManager.setHandler(EventTarget.messagesRead, _onMessagesRead);
|
||||||
final http.Response response;
|
_hubManager.setHandler(EventTarget.receiveStatusUpdate, _onReceiveStatusUpdate);
|
||||||
try {
|
_hubManager.setHandler(EventTarget.receiveSessionUpdate, _onReceiveSessionUpdate);
|
||||||
response = await http.post(
|
_hubManager.setHandler(EventTarget.removeSession, _onRemoveSession);
|
||||||
Uri.parse("${Config.neosHubUrl}/negotiate"),
|
|
||||||
headers: _apiClient.authorizationHeader,
|
await _hubManager.start();
|
||||||
);
|
await setUserStatus(userStatus);
|
||||||
_apiClient.checkResponse(response);
|
_hubManager.send(
|
||||||
} catch (e) {
|
"InitializeStatus",
|
||||||
throw "Failed to acquire connection info from Neos API: $e";
|
responseHandler: (Map data) async {
|
||||||
|
final rawContacts = data["contacts"] as List;
|
||||||
|
final contacts = rawContacts.map((e) => Friend.fromMap(e)).toList();
|
||||||
|
for (final contact in contacts) {
|
||||||
|
await _updateContact(contact);
|
||||||
}
|
}
|
||||||
final body = jsonDecode(response.body);
|
_initStatus = "";
|
||||||
final url = (body["url"] as String?)?.replaceFirst("https://", "wss://");
|
|
||||||
final wsToken = body["accessToken"];
|
|
||||||
|
|
||||||
if (url == null || wsToken == null) {
|
|
||||||
throw "Invalid response from server.";
|
|
||||||
}
|
|
||||||
final ws = await WebSocket.connect("$url&access_token=$wsToken");
|
|
||||||
_attempts = 0;
|
|
||||||
return ws;
|
|
||||||
} catch (e) {
|
|
||||||
final timeout = _reconnectTimeoutsSeconds[_attempts.clamp(0, _reconnectTimeoutsSeconds.length - 1)];
|
|
||||||
_logger.severe(e);
|
|
||||||
_logger.severe("Retrying in $timeout seconds");
|
|
||||||
await Future.delayed(Duration(seconds: timeout));
|
|
||||||
_attempts++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void _handleEvent(event) {
|
|
||||||
final body = jsonDecode((event.toString().replaceAll(_eofChar, "")));
|
|
||||||
final int rawType = body["type"] ?? 0;
|
|
||||||
if (rawType > EventType.values.length) {
|
|
||||||
_logger.info("Unhandled event type $rawType: $body");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
switch (EventType.values[rawType]) {
|
|
||||||
case EventType.unknown1:
|
|
||||||
case EventType.unknown2:
|
|
||||||
case EventType.unknown3:
|
|
||||||
case EventType.unknown4:
|
|
||||||
case EventType.unknown:
|
|
||||||
_logger.info("Received unknown event: $rawType: $body");
|
|
||||||
break;
|
|
||||||
case EventType.message:
|
|
||||||
_logger.info("Received message-event.");
|
|
||||||
_handleMessageEvent(body);
|
|
||||||
break;
|
|
||||||
case EventType.keepAlive:
|
|
||||||
_logger.info("Received keep-alive.");
|
|
||||||
break;
|
|
||||||
case EventType.error:
|
|
||||||
_logger.severe("Received error-event: ${body["error"]}");
|
|
||||||
// Should we trigger a manual reconnect here or just let the remote service close the connection?
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void _handleMessageEvent(body) async {
|
|
||||||
final target = EventTarget.parse(body["target"]);
|
|
||||||
final args = body["arguments"];
|
|
||||||
switch (target) {
|
|
||||||
case EventTarget.unknown:
|
|
||||||
_logger.info("Unknown event-target in message: $body");
|
|
||||||
return;
|
|
||||||
case EventTarget.messageSent:
|
|
||||||
final msg = args[0];
|
|
||||||
final message = Message.fromMap(msg, withState: MessageState.sent);
|
|
||||||
final cache = getUserMessageCache(message.recipientId) ?? _createUserMessageCache(message.recipientId);
|
|
||||||
cache.addMessage(message);
|
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
break;
|
await _refreshUnreads();
|
||||||
case EventTarget.receiveMessage:
|
_unreadSafeguard = Timer.periodic(_unreadSafeguardDuration, (timer) => _refreshUnreads());
|
||||||
final msg = args[0];
|
_hubManager.send("RequestStatus", arguments: [null, false]);
|
||||||
final message = Message.fromMap(msg);
|
},
|
||||||
final cache = getUserMessageCache(message.senderId) ?? _createUserMessageCache(message.senderId);
|
);
|
||||||
cache.addMessage(message);
|
|
||||||
if (message.senderId != selectedFriend?.id) {
|
|
||||||
addUnread(message);
|
|
||||||
updateFriendStatus(message.senderId);
|
|
||||||
} else {
|
|
||||||
markMessagesRead(MarkReadBatch(senderId: message.senderId, ids: [message.id], readTime: DateTime.now()));
|
|
||||||
}
|
|
||||||
notifyListeners();
|
|
||||||
break;
|
|
||||||
case EventTarget.messagesRead:
|
|
||||||
final messageIds = args[0]["ids"] as List;
|
|
||||||
final recipientId = args[0]["recipientId"];
|
|
||||||
if (recipientId == null) break;
|
|
||||||
final cache = getUserMessageCache(recipientId);
|
|
||||||
if (cache == null) break;
|
|
||||||
for (var id in messageIds) {
|
|
||||||
cache.setMessageState(id, MessageState.read);
|
|
||||||
}
|
|
||||||
notifyListeners();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void _sendData(data) {
|
Map<String, Session> createSessionMap(String salt) {
|
||||||
if (_wsChannel == null) throw "Neos Hub is not connected";
|
return _sessionMap.map((key, value) => MapEntry(CryptoHelper.idHash(value.id + salt), value));
|
||||||
_wsChannel!.add(jsonEncode(data)+_eofChar);
|
}
|
||||||
|
|
||||||
|
void _onMessageSent(List args) {
|
||||||
|
final msg = args[0];
|
||||||
|
final message = Message.fromMap(msg, withState: MessageState.sent);
|
||||||
|
final cache = getUserMessageCache(message.recipientId) ?? _createUserMessageCache(message.recipientId);
|
||||||
|
cache.addMessage(message);
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onReceiveMessage(List args) {
|
||||||
|
final msg = args[0];
|
||||||
|
final message = Message.fromMap(msg);
|
||||||
|
final cache = getUserMessageCache(message.senderId) ?? _createUserMessageCache(message.senderId);
|
||||||
|
cache.addMessage(message);
|
||||||
|
if (message.senderId != selectedFriend?.id) {
|
||||||
|
addUnread(message);
|
||||||
|
updateFriendStatus(message.senderId);
|
||||||
|
} else {
|
||||||
|
markMessagesRead(MarkReadBatch(senderId: message.senderId, ids: [message.id], readTime: DateTime.now()));
|
||||||
|
}
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onMessagesRead(List args) {
|
||||||
|
final messageIds = args[0]["ids"] as List;
|
||||||
|
final recipientId = args[0]["recipientId"];
|
||||||
|
if (recipientId == null) return;
|
||||||
|
final cache = getUserMessageCache(recipientId);
|
||||||
|
if (cache == null) return;
|
||||||
|
for (var id in messageIds) {
|
||||||
|
cache.setMessageState(id, MessageState.read);
|
||||||
|
}
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onReceiveStatusUpdate(List args) {
|
||||||
|
final statusUpdate = args[0];
|
||||||
|
var status = UserStatus.fromMap(statusUpdate);
|
||||||
|
final sessionMap = createSessionMap(status.hashSalt);
|
||||||
|
status = status.copyWith(
|
||||||
|
sessionData: status.sessions.map((e) => sessionMap[e.sessionHash] ?? Session.none()).toList());
|
||||||
|
final friend = getAsFriend(statusUpdate["userId"])?.copyWith(userStatus: status);
|
||||||
|
if (friend != null) {
|
||||||
|
_updateContact(friend);
|
||||||
|
}
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onReceiveSessionUpdate(List args) {
|
||||||
|
final sessionUpdate = args[0];
|
||||||
|
final session = Session.fromMap(sessionUpdate);
|
||||||
|
_sessionMap[session.id] = session;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onRemoveSession(List args) {
|
||||||
|
final session = args[0];
|
||||||
|
_sessionMap.remove(session);
|
||||||
|
notifyListeners();
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,8 +1,8 @@
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
|
||||||
import 'package:contacts_plus_plus/auxiliary.dart';
|
import 'package:recon/auxiliary.dart';
|
||||||
import 'package:contacts_plus_plus/models/message.dart';
|
import 'package:recon/models/message.dart';
|
||||||
import 'package:contacts_plus_plus/models/session.dart';
|
import 'package:recon/models/session.dart';
|
||||||
import 'package:flutter_local_notifications/flutter_local_notifications.dart' as fln;
|
import 'package:flutter_local_notifications/flutter_local_notifications.dart' as fln;
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
|
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
import 'package:contacts_plus_plus/apis/session_api.dart';
|
import 'package:recon/apis/session_api.dart';
|
||||||
import 'package:contacts_plus_plus/clients/api_client.dart';
|
import 'package:recon/clients/api_client.dart';
|
||||||
import 'package:contacts_plus_plus/clients/settings_client.dart';
|
import 'package:recon/clients/settings_client.dart';
|
||||||
import 'package:contacts_plus_plus/models/session.dart';
|
import 'package:recon/models/session.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
|
|
||||||
class SessionClient extends ChangeNotifier {
|
class SessionClient extends ChangeNotifier {
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
|
||||||
import 'package:contacts_plus_plus/models/settings.dart';
|
import 'package:recon/models/settings.dart';
|
||||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,13 +1,9 @@
|
||||||
class Config {
|
class Config {
|
||||||
static const String apiBaseUrl = "https://api.neos.com";
|
static const String apiBaseUrl = "https://api.resonite.com";
|
||||||
static const String legacyCloudUrl = "https://neoscloud.blob.core.windows.net/assets/";
|
static const String skyfrostAssetsUrl = "https://assets.resonite.com";
|
||||||
static const String blobStorageUrl = "https://cloudxstorage.blob.core.windows.net/assets/";
|
static const String resoniteHubUrl = "$apiBaseUrl/hub";
|
||||||
static const String videoStorageUrl = "https://cloudx-video.azureedge.net/";
|
|
||||||
static const String neosCdnUrl = "https://cloudx.azureedge.net/assets/";
|
|
||||||
static const String neosAssetsUrl = "https://cloudxstorage.blob.core.windows.net/assets/";
|
|
||||||
static const String neosHubUrl = "$apiBaseUrl/hub";
|
|
||||||
|
|
||||||
static const int messageCacheValiditySeconds = 90;
|
static const int messageCacheValiditySeconds = 90;
|
||||||
|
|
||||||
static const String latestCompatHash = "jnnkdwkBqGv5+jlf1u/k7A==";
|
static const String latestCompatHash = "YPDxN4N9fu7ZgV+Nr/AHQw==";
|
||||||
}
|
}
|
14
lib/crypto_helper.dart
Normal file
|
@ -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 'dart:developer';
|
||||||
|
|
||||||
import 'package:contacts_plus_plus/apis/github_api.dart';
|
import 'package:recon/apis/github_api.dart';
|
||||||
import 'package:contacts_plus_plus/client_holder.dart';
|
import 'package:recon/client_holder.dart';
|
||||||
import 'package:contacts_plus_plus/clients/api_client.dart';
|
import 'package:recon/clients/api_client.dart';
|
||||||
import 'package:contacts_plus_plus/clients/inventory_client.dart';
|
import 'package:recon/clients/inventory_client.dart';
|
||||||
import 'package:contacts_plus_plus/clients/messaging_client.dart';
|
import 'package:recon/clients/messaging_client.dart';
|
||||||
import 'package:contacts_plus_plus/clients/session_client.dart';
|
import 'package:recon/clients/session_client.dart';
|
||||||
import 'package:contacts_plus_plus/clients/settings_client.dart';
|
import 'package:recon/clients/settings_client.dart';
|
||||||
import 'package:contacts_plus_plus/models/sem_ver.dart';
|
import 'package:recon/models/sem_ver.dart';
|
||||||
import 'package:contacts_plus_plus/widgets/homepage.dart';
|
import 'package:recon/widgets/homepage.dart';
|
||||||
import 'package:contacts_plus_plus/widgets/login_screen.dart';
|
import 'package:recon/widgets/login_screen.dart';
|
||||||
import 'package:contacts_plus_plus/widgets/update_notifier.dart';
|
import 'package:recon/widgets/update_notifier.dart';
|
||||||
import 'package:dynamic_color/dynamic_color.dart';
|
import 'package:dynamic_color/dynamic_color.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter_downloader/flutter_downloader.dart';
|
import 'package:flutter_downloader/flutter_downloader.dart';
|
||||||
import 'package:flutter_phoenix/flutter_phoenix.dart';
|
import 'package:flutter_phoenix/flutter_phoenix.dart';
|
||||||
import 'package:hive_flutter/hive_flutter.dart';
|
import 'package:hive_flutter/hive_flutter.dart';
|
||||||
|
@ -47,20 +48,20 @@ void main() async {
|
||||||
cachedAuth = await ApiClient.tryCachedLogin();
|
cachedAuth = await ApiClient.tryCachedLogin();
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
|
|
||||||
runApp(ContactsPlusPlus(settingsClient: settingsClient, cachedAuthentication: cachedAuth));
|
runApp(ReCon(settingsClient: settingsClient, cachedAuthentication: cachedAuth));
|
||||||
}
|
}
|
||||||
|
|
||||||
class ContactsPlusPlus extends StatefulWidget {
|
class ReCon extends StatefulWidget {
|
||||||
const ContactsPlusPlus({required this.settingsClient, required this.cachedAuthentication, super.key});
|
const ReCon({required this.settingsClient, required this.cachedAuthentication, super.key});
|
||||||
|
|
||||||
final SettingsClient settingsClient;
|
final SettingsClient settingsClient;
|
||||||
final AuthenticationData cachedAuthentication;
|
final AuthenticationData cachedAuthentication;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<ContactsPlusPlus> createState() => _ContactsPlusPlusState();
|
State<ReCon> createState() => _ReConState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _ContactsPlusPlusState extends State<ContactsPlusPlus> {
|
class _ReConState extends State<ReCon> {
|
||||||
final Typography _typography = Typography.material2021(platform: TargetPlatform.android);
|
final Typography _typography = Typography.material2021(platform: TargetPlatform.android);
|
||||||
late AuthenticationData _authData = widget.cachedAuthentication;
|
late AuthenticationData _authData = widget.cachedAuthentication;
|
||||||
bool _checkedForUpdate = false;
|
bool _checkedForUpdate = false;
|
||||||
|
@ -128,7 +129,7 @@ class _ContactsPlusPlusState extends State<ContactsPlusPlus> {
|
||||||
child: DynamicColorBuilder(
|
child: DynamicColorBuilder(
|
||||||
builder: (ColorScheme? lightDynamic, ColorScheme? darkDynamic) => MaterialApp(
|
builder: (ColorScheme? lightDynamic, ColorScheme? darkDynamic) => MaterialApp(
|
||||||
debugShowCheckedModeBanner: false,
|
debugShowCheckedModeBanner: false,
|
||||||
title: 'Contacts++',
|
title: 'ReCon',
|
||||||
theme: ThemeData(
|
theme: ThemeData(
|
||||||
useMaterial3: true,
|
useMaterial3: true,
|
||||||
textTheme: _typography.black,
|
textTheme: _typography.black,
|
||||||
|
|
|
@ -1,35 +1,50 @@
|
||||||
class AuthenticationData {
|
class AuthenticationData {
|
||||||
static const _unauthenticated = AuthenticationData(userId: "", token: "", secretMachineId: "", isAuthenticated: false);
|
static const _unauthenticated = AuthenticationData(
|
||||||
|
userId: "",
|
||||||
|
token: "",
|
||||||
|
secretMachineIdHash: "",
|
||||||
|
isAuthenticated: false,
|
||||||
|
uid: "",
|
||||||
|
);
|
||||||
final String userId;
|
final String userId;
|
||||||
final String token;
|
final String token;
|
||||||
final String secretMachineId;
|
final String secretMachineIdHash;
|
||||||
final bool isAuthenticated;
|
final bool isAuthenticated;
|
||||||
|
final String uid;
|
||||||
|
|
||||||
const AuthenticationData({
|
const AuthenticationData({
|
||||||
required this.userId, required this.token, required this.secretMachineId, required this.isAuthenticated
|
required this.userId,
|
||||||
|
required this.token,
|
||||||
|
required this.secretMachineIdHash,
|
||||||
|
required this.isAuthenticated,
|
||||||
|
required this.uid,
|
||||||
});
|
});
|
||||||
|
|
||||||
factory AuthenticationData.fromMap(Map map) {
|
factory AuthenticationData.fromMap(Map map) {
|
||||||
|
map = map["entity"];
|
||||||
final userId = map["userId"];
|
final userId = map["userId"];
|
||||||
final token = map["token"];
|
final token = map["token"];
|
||||||
final machineId = map["secretMachineId"];
|
final machineId = map["secretMachineIdHash"];
|
||||||
if (userId == null || token == null || machineId == null) {
|
final uid = map["uid"];
|
||||||
|
if (userId == null || token == null || machineId == null || uid == null) {
|
||||||
return _unauthenticated;
|
return _unauthenticated;
|
||||||
}
|
}
|
||||||
return AuthenticationData(userId: userId, token: token, secretMachineId: machineId, isAuthenticated: true);
|
return AuthenticationData(userId: userId, token: token, secretMachineIdHash: machineId, isAuthenticated: true, uid: uid);
|
||||||
}
|
}
|
||||||
|
|
||||||
factory AuthenticationData.unauthenticated() => _unauthenticated;
|
factory AuthenticationData.unauthenticated() => _unauthenticated;
|
||||||
|
|
||||||
Map<String, String> get authorizationHeader => {
|
Map<String, String> get authorizationHeader => {
|
||||||
"Authorization": "neos $userId:$token"
|
"Authorization": "res $userId:$token",
|
||||||
};
|
"UID": uid,
|
||||||
|
};
|
||||||
|
|
||||||
Map<String, dynamic> toMap() {
|
Map<String, dynamic> toMap() {
|
||||||
return {
|
return {
|
||||||
"userId": userId,
|
"userId": userId,
|
||||||
"token": token,
|
"token": token,
|
||||||
"secretMachineId": secretMachineId,
|
"secretMachineId": secretMachineIdHash,
|
||||||
|
"uid": uid,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
import 'package:contacts_plus_plus/auxiliary.dart';
|
import 'package:recon/auxiliary.dart';
|
||||||
|
|
||||||
class CloudVariable {
|
class CloudVariable {
|
||||||
final String ownerId;
|
final String ownerId;
|
||||||
|
|
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:async';
|
||||||
import 'dart:developer';
|
import 'dart:developer';
|
||||||
|
|
||||||
import 'package:contacts_plus_plus/clients/api_client.dart';
|
import 'package:recon/clients/api_client.dart';
|
||||||
import 'package:contacts_plus_plus/apis/message_api.dart';
|
import 'package:recon/apis/message_api.dart';
|
||||||
import 'package:contacts_plus_plus/auxiliary.dart';
|
import 'package:recon/auxiliary.dart';
|
||||||
import 'package:contacts_plus_plus/string_formatter.dart';
|
import 'package:recon/string_formatter.dart';
|
||||||
import 'package:uuid/uuid.dart';
|
import 'package:uuid/uuid.dart';
|
||||||
|
|
||||||
enum MessageType {
|
enum MessageType {
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import 'package:contacts_plus_plus/models/users/user_profile.dart';
|
import 'package:recon/models/users/user_profile.dart';
|
||||||
|
|
||||||
class PersonalProfile {
|
class PersonalProfile {
|
||||||
final String id;
|
final String id;
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
|
|
||||||
import 'package:contacts_plus_plus/models/records/neos_db_asset.dart';
|
import 'package:recon/models/records/resonite_db_asset.dart';
|
||||||
|
|
||||||
class AssetDiff extends NeosDBAsset{
|
class AssetDiff extends ResoniteDBAsset{
|
||||||
final Diff state;
|
final Diff state;
|
||||||
final bool isUploaded;
|
final bool isUploaded;
|
||||||
|
|
||||||
|
|
|
@ -1,25 +1,25 @@
|
||||||
|
|
||||||
import 'dart:typed_data';
|
import 'dart:typed_data';
|
||||||
|
|
||||||
import 'package:contacts_plus_plus/models/records/neos_db_asset.dart';
|
import 'package:recon/models/records/resonite_db_asset.dart';
|
||||||
import 'package:path/path.dart';
|
import 'package:path/path.dart';
|
||||||
|
|
||||||
class AssetDigest {
|
class AssetDigest {
|
||||||
final Uint8List data;
|
final Uint8List data;
|
||||||
final NeosDBAsset asset;
|
final ResoniteDBAsset asset;
|
||||||
final String name;
|
final String name;
|
||||||
final String dbUri;
|
final String dbUri;
|
||||||
|
|
||||||
AssetDigest({required this.data, required this.asset, required this.name, required this.dbUri});
|
AssetDigest({required this.data, required this.asset, required this.name, required this.dbUri});
|
||||||
|
|
||||||
static Future<AssetDigest> fromData(Uint8List data, String filename) async {
|
static Future<AssetDigest> fromData(Uint8List data, String filename) async {
|
||||||
final asset = NeosDBAsset.fromData(data);
|
final asset = ResoniteDBAsset.fromData(data);
|
||||||
|
|
||||||
return AssetDigest(
|
return AssetDigest(
|
||||||
data: data,
|
data: data,
|
||||||
asset: asset,
|
asset: asset,
|
||||||
name: basenameWithoutExtension(filename),
|
name: basenameWithoutExtension(filename),
|
||||||
dbUri: "neosdb:///${asset.hash}${extension(filename)}",
|
dbUri: "resdb:///${asset.hash}${extension(filename)}",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -2,7 +2,7 @@ import 'package:path/path.dart';
|
||||||
import 'package:uuid/uuid.dart';
|
import 'package:uuid/uuid.dart';
|
||||||
|
|
||||||
class JsonTemplate {
|
class JsonTemplate {
|
||||||
static const String thumbUrl = "neosdb:///8ed80703e48c3d1556093927b67298f3d5e10315e9f782ec56fc49d6366f09b7.webp";
|
static const String thumbUrl = "resdb:///8ed80703e48c3d1556093927b67298f3d5e10315e9f782ec56fc49d6366f09b7.webp";
|
||||||
final Map data;
|
final Map data;
|
||||||
|
|
||||||
JsonTemplate({required this.data});
|
JsonTemplate({required this.data});
|
||||||
|
@ -2371,7 +2371,7 @@ class JsonTemplate {
|
||||||
},
|
},
|
||||||
"URL": {
|
"URL": {
|
||||||
"ID": const Uuid().v4(),
|
"ID": const Uuid().v4(),
|
||||||
"Data": "@neosdb:///3738bf6fc560f7d08d872ce12b06f4d9337ac5da415b6de6008a49ca128658ec"
|
"Data": "@resdb:///3738bf6fc560f7d08d872ce12b06f4d9337ac5da415b6de6008a49ca128658ec"
|
||||||
},
|
},
|
||||||
"Readable": {
|
"Readable": {
|
||||||
"ID": const Uuid().v4(),
|
"ID": const Uuid().v4(),
|
||||||
|
@ -2444,7 +2444,7 @@ class JsonTemplate {
|
||||||
},
|
},
|
||||||
"URL": {
|
"URL": {
|
||||||
"ID": const Uuid().v4(),
|
"ID": const Uuid().v4(),
|
||||||
"Data": "@neosdb:///c801b8d2522fb554678f17f4597158b1af3f9be3abd6ce35d5a3112a81e2bf39"
|
"Data": "@resdb:///c801b8d2522fb554678f17f4597158b1af3f9be3abd6ce35d5a3112a81e2bf39"
|
||||||
},
|
},
|
||||||
"Padding": {
|
"Padding": {
|
||||||
"ID": const Uuid().v4(),
|
"ID": const Uuid().v4(),
|
||||||
|
@ -2478,7 +2478,7 @@ class JsonTemplate {
|
||||||
},
|
},
|
||||||
"URL": {
|
"URL": {
|
||||||
"ID": const Uuid().v4(),
|
"ID": const Uuid().v4(),
|
||||||
"Data": "@neosdb:///4cac521169034ddd416c6deffe2eb16234863761837df677a910697ec5babd25"
|
"Data": "@resdb:///4cac521169034ddd416c6deffe2eb16234863761837df677a910697ec5babd25"
|
||||||
},
|
},
|
||||||
"Padding": {
|
"Padding": {
|
||||||
"ID": const Uuid().v4(),
|
"ID": const Uuid().v4(),
|
||||||
|
@ -2512,7 +2512,7 @@ class JsonTemplate {
|
||||||
},
|
},
|
||||||
"URL": {
|
"URL": {
|
||||||
"ID": const Uuid().v4(),
|
"ID": const Uuid().v4(),
|
||||||
"Data": "@neosdb:///23e7ad7cb0a5a4cf75e07c9e0848b1eb06bba15e8fa9b8cb0579fc823c532927"
|
"Data": "@resdb:///23e7ad7cb0a5a4cf75e07c9e0848b1eb06bba15e8fa9b8cb0579fc823c532927"
|
||||||
},
|
},
|
||||||
"Padding": {
|
"Padding": {
|
||||||
"ID": const Uuid().v4(),
|
"ID": const Uuid().v4(),
|
||||||
|
@ -2546,7 +2546,7 @@ class JsonTemplate {
|
||||||
},
|
},
|
||||||
"URL": {
|
"URL": {
|
||||||
"ID": const Uuid().v4(),
|
"ID": const Uuid().v4(),
|
||||||
"Data": "@neosdb:///415dc6290378574135b64c808dc640c1df7531973290c4970c51fdeb849cb0c5"
|
"Data": "@resdb:///415dc6290378574135b64c808dc640c1df7531973290c4970c51fdeb849cb0c5"
|
||||||
},
|
},
|
||||||
"Padding": {
|
"Padding": {
|
||||||
"ID": const Uuid().v4(),
|
"ID": const Uuid().v4(),
|
||||||
|
@ -2580,7 +2580,7 @@ class JsonTemplate {
|
||||||
},
|
},
|
||||||
"URL": {
|
"URL": {
|
||||||
"ID": const Uuid().v4(),
|
"ID": const Uuid().v4(),
|
||||||
"Data": "@neosdb:///bcda0bcc22bab28ea4fedae800bfbf9ec76d71cc3b9f851779a35b7e438a839d"
|
"Data": "@resdb:///bcda0bcc22bab28ea4fedae800bfbf9ec76d71cc3b9f851779a35b7e438a839d"
|
||||||
},
|
},
|
||||||
"Padding": {
|
"Padding": {
|
||||||
"ID": const Uuid().v4(),
|
"ID": const Uuid().v4(),
|
||||||
|
@ -2720,7 +2720,7 @@ class JsonTemplate {
|
||||||
},
|
},
|
||||||
"URL": {
|
"URL": {
|
||||||
"ID": const Uuid().v4(),
|
"ID": const Uuid().v4(),
|
||||||
"Data": "@neosdb:///274f0d4ea4bce93abc224c9ae9f9a97a9a396b382c5338f71c738d1591dd5c35.webp"
|
"Data": "@resdb:///274f0d4ea4bce93abc224c9ae9f9a97a9a396b382c5338f71c738d1591dd5c35.webp"
|
||||||
},
|
},
|
||||||
"FilterMode": {
|
"FilterMode": {
|
||||||
"ID": const Uuid().v4(),
|
"ID": const Uuid().v4(),
|
||||||
|
|
|
@ -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
|
enum RecordPreprocessState
|
||||||
{
|
{
|
||||||
|
|
|
@ -1,10 +1,9 @@
|
||||||
import 'package:contacts_plus_plus/auxiliary.dart';
|
import 'package:recon/auxiliary.dart';
|
||||||
import 'package:contacts_plus_plus/models/message.dart';
|
import 'package:recon/models/message.dart';
|
||||||
import 'package:contacts_plus_plus/models/records/asset_digest.dart';
|
import 'package:recon/models/records/asset_digest.dart';
|
||||||
import 'package:contacts_plus_plus/models/records/neos_db_asset.dart';
|
import 'package:recon/models/records/resonite_db_asset.dart';
|
||||||
import 'package:contacts_plus_plus/string_formatter.dart';
|
import 'package:recon/string_formatter.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
|
||||||
import 'package:uuid/uuid.dart';
|
import 'package:uuid/uuid.dart';
|
||||||
|
|
||||||
enum RecordType {
|
enum RecordType {
|
||||||
|
@ -59,7 +58,7 @@ class Record {
|
||||||
isListed: false,
|
isListed: false,
|
||||||
isForPatreons: false,
|
isForPatreons: false,
|
||||||
lastModificationTime: DateTimeX.epoch,
|
lastModificationTime: DateTimeX.epoch,
|
||||||
neosDBManifest: [],
|
resoniteDBManifest: [],
|
||||||
lastModifyingUserId: "",
|
lastModifyingUserId: "",
|
||||||
lastModifyingMachineId: "",
|
lastModifyingMachineId: "",
|
||||||
creationTime: DateTimeX.epoch,
|
creationTime: DateTimeX.epoch,
|
||||||
|
@ -100,7 +99,7 @@ class Record {
|
||||||
final int rating;
|
final int rating;
|
||||||
final int randomOrder;
|
final int randomOrder;
|
||||||
final List<String> manifest;
|
final List<String> manifest;
|
||||||
final List<NeosDBAsset> neosDBManifest;
|
final List<ResoniteDBAsset> resoniteDBManifest;
|
||||||
final String url;
|
final String url;
|
||||||
final bool isValidOwnerId;
|
final bool isValidOwnerId;
|
||||||
final bool isValidRecordId;
|
final bool isValidRecordId;
|
||||||
|
@ -122,7 +121,7 @@ class Record {
|
||||||
required this.isListed,
|
required this.isListed,
|
||||||
required this.isForPatreons,
|
required this.isForPatreons,
|
||||||
required this.lastModificationTime,
|
required this.lastModificationTime,
|
||||||
required this.neosDBManifest,
|
required this.resoniteDBManifest,
|
||||||
required this.lastModifyingUserId,
|
required this.lastModifyingUserId,
|
||||||
required this.lastModifyingMachineId,
|
required this.lastModifyingMachineId,
|
||||||
required this.creationTime,
|
required this.creationTime,
|
||||||
|
@ -153,14 +152,14 @@ class Record {
|
||||||
combinedRecordId: combinedRecordId,
|
combinedRecordId: combinedRecordId,
|
||||||
assetUri: assetUri,
|
assetUri: assetUri,
|
||||||
name: filename,
|
name: filename,
|
||||||
tags: ([filename, "message_item", "message_id:${Message.generateId()}", "contacts-plus-plus"] + (extraTags ?? []))
|
tags: ([filename, "message_item", "message_id:${Message.generateId()}", "recon"] + (extraTags ?? []))
|
||||||
.unique(),
|
.unique(),
|
||||||
recordType: recordType,
|
recordType: recordType,
|
||||||
thumbnailUri: thumbnailUri,
|
thumbnailUri: thumbnailUri,
|
||||||
isPublic: false,
|
isPublic: false,
|
||||||
isForPatreons: false,
|
isForPatreons: false,
|
||||||
isListed: false,
|
isListed: false,
|
||||||
neosDBManifest: digests.map((e) => e.asset).toList(),
|
resoniteDBManifest: digests.map((e) => e.asset).toList(),
|
||||||
globalVersion: 0,
|
globalVersion: 0,
|
||||||
localVersion: 1,
|
localVersion: 1,
|
||||||
lastModifyingUserId: userId,
|
lastModifyingUserId: userId,
|
||||||
|
@ -173,7 +172,7 @@ class Record {
|
||||||
path: '',
|
path: '',
|
||||||
description: '',
|
description: '',
|
||||||
manifest: digests.map((e) => e.dbUri).toList(),
|
manifest: digests.map((e) => e.dbUri).toList(),
|
||||||
url: "neosrec:///$userId/${combinedRecordId.id}",
|
url: "resrec:///$userId/${combinedRecordId.id}",
|
||||||
isValidOwnerId: true,
|
isValidOwnerId: true,
|
||||||
isValidRecordId: true,
|
isValidRecordId: true,
|
||||||
visits: 0,
|
visits: 0,
|
||||||
|
@ -199,14 +198,14 @@ class Record {
|
||||||
isForPatreons: map["isForPatreons"] ?? false,
|
isForPatreons: map["isForPatreons"] ?? false,
|
||||||
isListed: map["isListed"] ?? false,
|
isListed: map["isListed"] ?? false,
|
||||||
lastModificationTime: DateTime.tryParse(map["lastModificationTime"]) ?? DateTimeX.epoch,
|
lastModificationTime: DateTime.tryParse(map["lastModificationTime"]) ?? DateTimeX.epoch,
|
||||||
neosDBManifest: (map["neosDBManifest"] as List? ?? []).map((e) => NeosDBAsset.fromMap(e)).toList(),
|
resoniteDBManifest: (map["resoniteDBManifest"] as List? ?? []).map((e) => ResoniteDBAsset.fromMap(e)).toList(),
|
||||||
lastModifyingUserId: map["lastModifyingUserId"] ?? "",
|
lastModifyingUserId: map["lastModifyingUserId"] ?? "",
|
||||||
lastModifyingMachineId: map["lastModifyingMachineId"] ?? "",
|
lastModifyingMachineId: map["lastModifyingMachineId"] ?? "",
|
||||||
creationTime: DateTime.tryParse(map["lastModificationTime"]) ?? DateTimeX.epoch,
|
creationTime: DateTime.tryParse(map["lastModificationTime"]) ?? DateTimeX.epoch,
|
||||||
isSynced: map["isSynced"] ?? false,
|
isSynced: map["isSynced"] ?? false,
|
||||||
fetchedOn: DateTime.tryParse(map["fetchedOn"] ?? "") ?? DateTimeX.epoch,
|
fetchedOn: DateTime.tryParse(map["fetchedOn"] ?? "") ?? DateTimeX.epoch,
|
||||||
path: map["path"] ?? "",
|
path: map["path"] ?? "",
|
||||||
manifest: (map["neosDBManifest"] as List? ?? []).map((e) => e.toString()).toList(),
|
manifest: (map["resoniteDBManifest"] as List? ?? []).map((e) => e.toString()).toList(),
|
||||||
url: map["url"] ?? "",
|
url: map["url"] ?? "",
|
||||||
isValidOwnerId: map["isValidOwnerId"] == "true",
|
isValidOwnerId: map["isValidOwnerId"] == "true",
|
||||||
isValidRecordId: map["isValidRecordId"] == "true",
|
isValidRecordId: map["isValidRecordId"] == "true",
|
||||||
|
@ -220,7 +219,7 @@ class Record {
|
||||||
bool get isRoot => this == _rootRecord;
|
bool get isRoot => this == _rootRecord;
|
||||||
|
|
||||||
String get linkRecordId {
|
String get linkRecordId {
|
||||||
if (!assetUri.startsWith("neosrec")) {
|
if (!assetUri.startsWith("resrec")) {
|
||||||
throw "Record is not a link.";
|
throw "Record is not a link.";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -233,11 +232,11 @@ class Record {
|
||||||
}
|
}
|
||||||
|
|
||||||
String get linkOwnerId {
|
String get linkOwnerId {
|
||||||
if (!assetUri.startsWith("neosrec")) {
|
if (!assetUri.startsWith("resrec")) {
|
||||||
throw "Record is not a link.";
|
throw "Record is not a link.";
|
||||||
}
|
}
|
||||||
|
|
||||||
String ownerId = assetUri.replaceFirst("neosrec:///", "");
|
String ownerId = assetUri.replaceFirst("resrec:///", "");
|
||||||
|
|
||||||
final lastSlashIdx = ownerId.lastIndexOf("/");
|
final lastSlashIdx = ownerId.lastIndexOf("/");
|
||||||
if (lastSlashIdx == -1) {
|
if (lastSlashIdx == -1) {
|
||||||
|
@ -265,7 +264,7 @@ class Record {
|
||||||
bool? isListed,
|
bool? isListed,
|
||||||
bool? isDeleted,
|
bool? isDeleted,
|
||||||
DateTime? lastModificationTime,
|
DateTime? lastModificationTime,
|
||||||
List<NeosDBAsset>? neosDBManifest,
|
List<ResoniteDBAsset>? resoniteDBManifest,
|
||||||
String? lastModifyingUserId,
|
String? lastModifyingUserId,
|
||||||
String? lastModifyingMachineId,
|
String? lastModifyingMachineId,
|
||||||
DateTime? creationTime,
|
DateTime? creationTime,
|
||||||
|
@ -296,7 +295,7 @@ class Record {
|
||||||
isForPatreons: isForPatreons ?? this.isForPatreons,
|
isForPatreons: isForPatreons ?? this.isForPatreons,
|
||||||
isListed: isListed ?? this.isListed,
|
isListed: isListed ?? this.isListed,
|
||||||
lastModificationTime: lastModificationTime ?? this.lastModificationTime,
|
lastModificationTime: lastModificationTime ?? this.lastModificationTime,
|
||||||
neosDBManifest: neosDBManifest ?? this.neosDBManifest,
|
resoniteDBManifest: resoniteDBManifest ?? this.resoniteDBManifest,
|
||||||
lastModifyingUserId: lastModifyingUserId ?? this.lastModifyingUserId,
|
lastModifyingUserId: lastModifyingUserId ?? this.lastModifyingUserId,
|
||||||
lastModifyingMachineId: lastModifyingMachineId ?? this.lastModifyingMachineId,
|
lastModifyingMachineId: lastModifyingMachineId ?? this.lastModifyingMachineId,
|
||||||
creationTime: creationTime ?? this.creationTime,
|
creationTime: creationTime ?? this.creationTime,
|
||||||
|
@ -330,7 +329,7 @@ class Record {
|
||||||
"isForPatreons": isForPatreons,
|
"isForPatreons": isForPatreons,
|
||||||
"isListed": isListed,
|
"isListed": isListed,
|
||||||
"lastModificationTime": lastModificationTime.toUtc().toIso8601String(),
|
"lastModificationTime": lastModificationTime.toUtc().toIso8601String(),
|
||||||
"neosDBManifest": neosDBManifest.map((e) => e.toMap()).toList(),
|
"resoniteDBManifest": resoniteDBManifest.map((e) => e.toMap()).toList(),
|
||||||
"lastModifyingUserId": lastModifyingUserId,
|
"lastModifyingUserId": lastModifyingUserId,
|
||||||
"lastModifyingMachineId": lastModifyingMachineId,
|
"lastModifyingMachineId": lastModifyingMachineId,
|
||||||
"creationTime": creationTime.toUtc().toIso8601String(),
|
"creationTime": creationTime.toUtc().toIso8601String(),
|
||||||
|
|
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 'dart:convert';
|
||||||
import 'package:contacts_plus_plus/string_formatter.dart';
|
|
||||||
|
import 'package:recon/string_formatter.dart';
|
||||||
|
import 'package:crypto/crypto.dart';
|
||||||
|
|
||||||
class Session {
|
class Session {
|
||||||
final String id;
|
final String id;
|
||||||
final String name;
|
final String name;
|
||||||
final FormatNode formattedName;
|
final FormatNode formattedName;
|
||||||
final List<SessionUser> sessionUsers;
|
final List<SessionUser> sessionUsers;
|
||||||
final String thumbnail;
|
final String thumbnailUrl;
|
||||||
final int maxUsers;
|
final int maxUsers;
|
||||||
final bool hasEnded;
|
final bool hasEnded;
|
||||||
final bool isValid;
|
final bool isValid;
|
||||||
|
@ -22,7 +24,7 @@ class Session {
|
||||||
required this.id,
|
required this.id,
|
||||||
required this.name,
|
required this.name,
|
||||||
required this.sessionUsers,
|
required this.sessionUsers,
|
||||||
required this.thumbnail,
|
required this.thumbnailUrl,
|
||||||
required this.maxUsers,
|
required this.maxUsers,
|
||||||
required this.hasEnded,
|
required this.hasEnded,
|
||||||
required this.isValid,
|
required this.isValid,
|
||||||
|
@ -40,7 +42,7 @@ class Session {
|
||||||
id: "",
|
id: "",
|
||||||
name: "",
|
name: "",
|
||||||
sessionUsers: const [],
|
sessionUsers: const [],
|
||||||
thumbnail: "",
|
thumbnailUrl: "",
|
||||||
maxUsers: 0,
|
maxUsers: 0,
|
||||||
hasEnded: true,
|
hasEnded: true,
|
||||||
isValid: false,
|
isValid: false,
|
||||||
|
@ -60,7 +62,7 @@ class Session {
|
||||||
id: map["sessionId"],
|
id: map["sessionId"],
|
||||||
name: map["name"],
|
name: map["name"],
|
||||||
sessionUsers: (map["sessionUsers"] as List? ?? []).map((entry) => SessionUser.fromMap(entry)).toList(),
|
sessionUsers: (map["sessionUsers"] as List? ?? []).map((entry) => SessionUser.fromMap(entry)).toList(),
|
||||||
thumbnail: map["thumbnail"] ?? "",
|
thumbnailUrl: map["thumbnailUrl"] ?? "",
|
||||||
maxUsers: map["maxUsers"] ?? 0,
|
maxUsers: map["maxUsers"] ?? 0,
|
||||||
hasEnded: map["hasEnded"] ?? false,
|
hasEnded: map["hasEnded"] ?? false,
|
||||||
isValid: map["isValid"] ?? true,
|
isValid: map["isValid"] ?? true,
|
||||||
|
@ -78,7 +80,7 @@ class Session {
|
||||||
"sessionId": id,
|
"sessionId": id,
|
||||||
"name": name,
|
"name": name,
|
||||||
"sessionUsers": shallow ? [] : sessionUsers.map((e) => e.toMap()).toList(),
|
"sessionUsers": shallow ? [] : sessionUsers.map((e) => e.toMap()).toList(),
|
||||||
"thumbnail": thumbnail,
|
"thumbnail": thumbnailUrl,
|
||||||
"maxUsers": maxUsers,
|
"maxUsers": maxUsers,
|
||||||
"hasEnded": hasEnded,
|
"hasEnded": hasEnded,
|
||||||
"isValid": isValid,
|
"isValid": isValid,
|
||||||
|
@ -97,15 +99,17 @@ class Session {
|
||||||
enum SessionAccessLevel {
|
enum SessionAccessLevel {
|
||||||
unknown,
|
unknown,
|
||||||
private,
|
private,
|
||||||
friends,
|
contacts,
|
||||||
friendsOfFriends,
|
contactsPlus,
|
||||||
|
registeredUsers,
|
||||||
anyone;
|
anyone;
|
||||||
|
|
||||||
static const _readableNamesMap = {
|
static const _readableNamesMap = {
|
||||||
SessionAccessLevel.unknown: "Unknown",
|
SessionAccessLevel.unknown: "Unknown",
|
||||||
SessionAccessLevel.private: "Private",
|
SessionAccessLevel.private: "Private",
|
||||||
SessionAccessLevel.friends: "Contacts",
|
SessionAccessLevel.contacts: "Contacts",
|
||||||
SessionAccessLevel.friendsOfFriends: "Contacts+",
|
SessionAccessLevel.contactsPlus: "Contacts+",
|
||||||
|
SessionAccessLevel.registeredUsers: "Registered users",
|
||||||
SessionAccessLevel.anyone: "Anyone",
|
SessionAccessLevel.anyone: "Anyone",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -117,7 +121,7 @@ enum SessionAccessLevel {
|
||||||
}
|
}
|
||||||
|
|
||||||
String toReadableString() {
|
String toReadableString() {
|
||||||
return SessionAccessLevel._readableNamesMap[this] ?? "Unknown";
|
return SessionAccessLevel._readableNamesMap[this] ?? SessionAccessLevel.unknown.toReadableString();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -177,7 +181,6 @@ class SessionFilterSettings {
|
||||||
String buildRequestString() => "?includeEmptyHeadless=$includeEmptyHeadless"
|
String buildRequestString() => "?includeEmptyHeadless=$includeEmptyHeadless"
|
||||||
"${"&includeEnded=$includeEnded"}"
|
"${"&includeEnded=$includeEnded"}"
|
||||||
"${name.isNotEmpty ? "&name=$name" : ""}"
|
"${name.isNotEmpty ? "&name=$name" : ""}"
|
||||||
"${!includeIncompatible ? "&compatibilityHash=${Uri.encodeComponent(Config.latestCompatHash)}" : ""}"
|
|
||||||
"${hostName.isNotEmpty ? "&hostName=$hostName" : ""}"
|
"${hostName.isNotEmpty ? "&hostName=$hostName" : ""}"
|
||||||
"${minActiveUsers > 0 ? "&minActiveUsers=$minActiveUsers" : ""}";
|
"${minActiveUsers > 0 ? "&minActiveUsers=$minActiveUsers" : ""}";
|
||||||
|
|
||||||
|
|
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 'dart:convert';
|
||||||
|
|
||||||
import 'package:contacts_plus_plus/models/sem_ver.dart';
|
import 'package:recon/models/sem_ver.dart';
|
||||||
import 'package:contacts_plus_plus/models/users/online_status.dart';
|
import 'package:recon/models/users/online_status.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:uuid/uuid.dart';
|
import 'package:uuid/uuid.dart';
|
||||||
|
|
||||||
|
|
|
@ -1,36 +1,36 @@
|
||||||
import 'package:contacts_plus_plus/auxiliary.dart';
|
import 'package:recon/auxiliary.dart';
|
||||||
import 'package:contacts_plus_plus/models/users/user_profile.dart';
|
import 'package:recon/models/users/user_profile.dart';
|
||||||
import 'package:contacts_plus_plus/models/users/friend_status.dart';
|
import 'package:recon/models/users/friend_status.dart';
|
||||||
import 'package:contacts_plus_plus/models/users/online_status.dart';
|
import 'package:recon/models/users/online_status.dart';
|
||||||
import 'package:contacts_plus_plus/models/users/user_status.dart';
|
import 'package:recon/models/users/user_status.dart';
|
||||||
|
|
||||||
class Friend implements Comparable {
|
class Friend implements Comparable {
|
||||||
static const _emptyId = "-1";
|
static const _emptyId = "-1";
|
||||||
static const _neosBotId = "U-Neos";
|
static const _resoniteBotId = "U-Resonite";
|
||||||
final String id;
|
final String id;
|
||||||
final String username;
|
final String username;
|
||||||
final String ownerId;
|
final String ownerId;
|
||||||
final UserStatus userStatus;
|
final UserStatus userStatus;
|
||||||
final UserProfile userProfile;
|
final UserProfile userProfile;
|
||||||
final FriendStatus friendStatus;
|
final FriendStatus contactStatus;
|
||||||
final DateTime latestMessageTime;
|
final DateTime latestMessageTime;
|
||||||
|
|
||||||
const Friend({required this.id, required this.username, required this.ownerId, required this.userStatus, required this.userProfile,
|
const Friend({required this.id, required this.username, required this.ownerId, required this.userStatus, required this.userProfile,
|
||||||
required this.friendStatus, required this.latestMessageTime,
|
required this.contactStatus, required this.latestMessageTime,
|
||||||
});
|
});
|
||||||
|
|
||||||
bool get isHeadless => userStatus.activeSessions.any((session) => session.headlessHost == true && session.hostUserId == id);
|
bool get isHeadless => userStatus.outputDevice == "Headless";
|
||||||
|
|
||||||
factory Friend.fromMap(Map map) {
|
factory Friend.fromMap(Map map) {
|
||||||
final userStatus = UserStatus.fromMap(map["userStatus"]);
|
var userStatus = map["userStatus"] == null ? UserStatus.empty() : UserStatus.fromMap(map["userStatus"]);
|
||||||
return Friend(
|
return Friend(
|
||||||
id: map["id"],
|
id: map["id"],
|
||||||
username: map["friendUsername"],
|
username: map["contactUsername"],
|
||||||
ownerId: map["ownerId"] ?? map["id"],
|
ownerId: map["ownerId"] ?? map["id"],
|
||||||
// Neos bot status is always offline but should be displayed as online
|
// Neos bot status is always offline but should be displayed as online
|
||||||
userStatus: map["id"] == _neosBotId ? userStatus.copyWith(onlineStatus: OnlineStatus.online) : userStatus,
|
userStatus: map["id"] == _resoniteBotId ? userStatus.copyWith(onlineStatus: OnlineStatus.online) : userStatus,
|
||||||
userProfile: UserProfile.fromMap(map["profile"] ?? {}),
|
userProfile: UserProfile.fromMap(map["profile"] ?? {}),
|
||||||
friendStatus: FriendStatus.fromString(map["friendStatus"]),
|
contactStatus: FriendStatus.fromString(map["contactStatus"]),
|
||||||
latestMessageTime: map["latestMessageTime"] == null
|
latestMessageTime: map["latestMessageTime"] == null
|
||||||
? DateTime.fromMillisecondsSinceEpoch(0) : DateTime.parse(map["latestMessageTime"]),
|
? DateTime.fromMillisecondsSinceEpoch(0) : DateTime.parse(map["latestMessageTime"]),
|
||||||
);
|
);
|
||||||
|
@ -48,7 +48,7 @@ class Friend implements Comparable {
|
||||||
ownerId: "",
|
ownerId: "",
|
||||||
userStatus: UserStatus.empty(),
|
userStatus: UserStatus.empty(),
|
||||||
userProfile: UserProfile.empty(),
|
userProfile: UserProfile.empty(),
|
||||||
friendStatus: FriendStatus.none,
|
contactStatus: FriendStatus.none,
|
||||||
latestMessageTime: DateTimeX.epoch
|
latestMessageTime: DateTimeX.epoch
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -57,14 +57,14 @@ class Friend implements Comparable {
|
||||||
|
|
||||||
Friend copyWith({
|
Friend copyWith({
|
||||||
String? id, String? username, String? ownerId, UserStatus? userStatus, UserProfile? userProfile,
|
String? id, String? username, String? ownerId, UserStatus? userStatus, UserProfile? userProfile,
|
||||||
FriendStatus? friendStatus, DateTime? latestMessageTime}) {
|
FriendStatus? contactStatus, DateTime? latestMessageTime}) {
|
||||||
return Friend(
|
return Friend(
|
||||||
id: id ?? this.id,
|
id: id ?? this.id,
|
||||||
username: username ?? this.username,
|
username: username ?? this.username,
|
||||||
ownerId: ownerId ?? this.ownerId,
|
ownerId: ownerId ?? this.ownerId,
|
||||||
userStatus: userStatus ?? this.userStatus,
|
userStatus: userStatus ?? this.userStatus,
|
||||||
userProfile: userProfile ?? this.userProfile,
|
userProfile: userProfile ?? this.userProfile,
|
||||||
friendStatus: friendStatus ?? this.friendStatus,
|
contactStatus: contactStatus ?? this.contactStatus,
|
||||||
latestMessageTime: latestMessageTime ?? this.latestMessageTime,
|
latestMessageTime: latestMessageTime ?? this.latestMessageTime,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -72,11 +72,11 @@ class Friend implements Comparable {
|
||||||
Map toMap({bool shallow=false}) {
|
Map toMap({bool shallow=false}) {
|
||||||
return {
|
return {
|
||||||
"id": id,
|
"id": id,
|
||||||
"username": username,
|
"contactUsername": username,
|
||||||
"ownerId": ownerId,
|
"ownerId": ownerId,
|
||||||
"userStatus": userStatus.toMap(shallow: shallow),
|
"userStatus": userStatus.toMap(shallow: shallow),
|
||||||
"profile": userProfile.toMap(),
|
"profile": userProfile.toMap(),
|
||||||
"friendStatus": friendStatus.name,
|
"contactStatus": contactStatus.name,
|
||||||
"latestMessageTime": latestMessageTime.toIso8601String(),
|
"latestMessageTime": latestMessageTime.toIso8601String(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,7 +19,7 @@ enum OnlineStatus {
|
||||||
|
|
||||||
factory OnlineStatus.fromString(String? text) {
|
factory OnlineStatus.fromString(String? text) {
|
||||||
return OnlineStatus.values.firstWhere((element) => element.name.toLowerCase() == text?.toLowerCase(),
|
return OnlineStatus.values.firstWhere((element) => element.name.toLowerCase() == text?.toLowerCase(),
|
||||||
orElse: () => OnlineStatus.offline,
|
orElse: () => OnlineStatus.online,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import 'package:contacts_plus_plus/models/users/user_profile.dart';
|
import 'package:recon/models/users/user_profile.dart';
|
||||||
|
|
||||||
class User {
|
class User {
|
||||||
final String id;
|
final String id;
|
||||||
|
|
|
@ -1,56 +1,100 @@
|
||||||
import 'package:contacts_plus_plus/models/session.dart';
|
import 'package:recon/crypto_helper.dart';
|
||||||
import 'package:contacts_plus_plus/models/users/online_status.dart';
|
import 'package:recon/models/session.dart';
|
||||||
|
import 'package:recon/models/session_metadata.dart';
|
||||||
|
import 'package:recon/models/users/online_status.dart';
|
||||||
|
import 'package:uuid/uuid.dart';
|
||||||
|
|
||||||
|
enum UserSessionType
|
||||||
|
{
|
||||||
|
unknown,
|
||||||
|
graphicalClient,
|
||||||
|
chatClient,
|
||||||
|
headless,
|
||||||
|
not;
|
||||||
|
|
||||||
|
factory UserSessionType.fromString(String? text) {
|
||||||
|
return UserSessionType.values.firstWhere((element) => element.name.toLowerCase() == text?.toLowerCase(),
|
||||||
|
orElse: () => UserSessionType.unknown,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class UserStatus {
|
class UserStatus {
|
||||||
final OnlineStatus onlineStatus;
|
final OnlineStatus onlineStatus;
|
||||||
final DateTime lastStatusChange;
|
final DateTime lastStatusChange;
|
||||||
final int currentSessionAccessLevel;
|
final DateTime lastPresenceTimestamp;
|
||||||
final bool currentSessionHidden;
|
final String userSessionId;
|
||||||
final bool currentHosting;
|
final int currentSessionIndex;
|
||||||
final Session currentSession;
|
final List<SessionMetadata> sessions;
|
||||||
final List<Session> activeSessions;
|
final String appVersion;
|
||||||
final String neosVersion;
|
|
||||||
final String outputDevice;
|
final String outputDevice;
|
||||||
final bool isMobile;
|
final bool isMobile;
|
||||||
|
final bool isPresent;
|
||||||
final String compatibilityHash;
|
final String compatibilityHash;
|
||||||
|
final String hashSalt;
|
||||||
|
final UserSessionType sessionType;
|
||||||
|
final List<Session> decodedSessions;
|
||||||
|
|
||||||
const UserStatus(
|
const UserStatus({
|
||||||
{required this.onlineStatus, required this.lastStatusChange, required this.currentSession,
|
required this.onlineStatus,
|
||||||
required this.currentSessionAccessLevel, required this.currentSessionHidden, required this.currentHosting,
|
required this.lastStatusChange,
|
||||||
required this.activeSessions, required this.neosVersion, required this.outputDevice, required this.isMobile,
|
required this.lastPresenceTimestamp,
|
||||||
required this.compatibilityHash,
|
required this.userSessionId,
|
||||||
});
|
required this.currentSessionIndex,
|
||||||
|
required this.sessions,
|
||||||
|
required this.appVersion,
|
||||||
|
required this.outputDevice,
|
||||||
|
required this.isMobile,
|
||||||
|
required this.isPresent,
|
||||||
|
required this.compatibilityHash,
|
||||||
|
required this.hashSalt,
|
||||||
|
required this.sessionType,
|
||||||
|
this.decodedSessions = const []
|
||||||
|
});
|
||||||
|
|
||||||
|
factory UserStatus.initial() =>
|
||||||
|
UserStatus.empty().copyWith(
|
||||||
|
onlineStatus: OnlineStatus.online,
|
||||||
|
hashSalt: CryptoHelper.cryptoToken(),
|
||||||
|
outputDevice: "Mobile",
|
||||||
|
userSessionId: const Uuid().v4().toString(),
|
||||||
|
sessionType: UserSessionType.chatClient,
|
||||||
|
);
|
||||||
|
|
||||||
factory UserStatus.empty() =>
|
factory UserStatus.empty() =>
|
||||||
UserStatus(
|
UserStatus(
|
||||||
onlineStatus: OnlineStatus.offline,
|
onlineStatus: OnlineStatus.offline,
|
||||||
lastStatusChange: DateTime.now(),
|
lastStatusChange: DateTime.now(),
|
||||||
currentSessionAccessLevel: 0,
|
lastPresenceTimestamp: DateTime.now(),
|
||||||
currentSessionHidden: false,
|
userSessionId: "",
|
||||||
currentHosting: false,
|
currentSessionIndex: -1,
|
||||||
currentSession: Session.none(),
|
sessions: [],
|
||||||
activeSessions: [],
|
appVersion: "",
|
||||||
neosVersion: "",
|
|
||||||
outputDevice: "Unknown",
|
outputDevice: "Unknown",
|
||||||
isMobile: false,
|
isMobile: false,
|
||||||
|
isPresent: false,
|
||||||
compatibilityHash: "",
|
compatibilityHash: "",
|
||||||
|
hashSalt: "",
|
||||||
|
sessionType: UserSessionType.unknown
|
||||||
);
|
);
|
||||||
|
|
||||||
factory UserStatus.fromMap(Map map) {
|
factory UserStatus.fromMap(Map map) {
|
||||||
final statusString = map["onlineStatus"] as String?;
|
final statusString = map["onlineStatus"].toString();
|
||||||
final status = OnlineStatus.fromString(statusString);
|
final status = OnlineStatus.fromString(statusString);
|
||||||
return UserStatus(
|
return UserStatus(
|
||||||
onlineStatus: status,
|
onlineStatus: status,
|
||||||
lastStatusChange: DateTime.parse(map["lastStatusChange"]),
|
lastStatusChange: DateTime.tryParse(map["lastStatusChange"] ?? "") ?? DateTime.now(),
|
||||||
currentSessionAccessLevel: map["currentSessionAccessLevel"] ?? 0,
|
lastPresenceTimestamp: DateTime.tryParse(map["lastPresenceTimestamp"] ?? "") ?? DateTime.now(),
|
||||||
currentSessionHidden: map["currentSessionHidden"] ?? false,
|
userSessionId: map["userSessionId"] ?? "",
|
||||||
currentHosting: map["currentHosting"] ?? false,
|
isPresent: map["isPresent"] ?? false,
|
||||||
currentSession: Session.fromMap(map["currentSession"]),
|
currentSessionIndex: map["currentSessionIndex"] ?? -1,
|
||||||
activeSessions: (map["activeSessions"] as List? ?? []).map((e) => Session.fromMap(e)).toList(),
|
sessions: (map["sessions"] as List? ?? []).map((e) => SessionMetadata.fromMap(e)).toList(),
|
||||||
neosVersion: map["neosVersion"] ?? "",
|
appVersion: map["appVersion"] ?? "",
|
||||||
outputDevice: map["outputDevice"] ?? "Unknown",
|
outputDevice: map["outputDevice"] ?? "Unknown",
|
||||||
isMobile: map["isMobile"] ?? false,
|
isMobile: map["isMobile"] ?? false,
|
||||||
compatibilityHash: map["compatabilityHash"] ?? ""
|
compatibilityHash: map["compatabilityHash"] ?? "",
|
||||||
|
hashSalt: map["hashSalt"] ?? "",
|
||||||
|
sessionType: UserSessionType.fromString(map["sessionType"])
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -58,12 +102,18 @@ class UserStatus {
|
||||||
return {
|
return {
|
||||||
"onlineStatus": onlineStatus.index,
|
"onlineStatus": onlineStatus.index,
|
||||||
"lastStatusChange": lastStatusChange.toIso8601String(),
|
"lastStatusChange": lastStatusChange.toIso8601String(),
|
||||||
"currentSessionAccessLevel": currentSessionAccessLevel,
|
"isPresent": isPresent,
|
||||||
"currentSessionHidden": currentSessionHidden,
|
"lastPresenceTimestamp": lastPresenceTimestamp.toIso8601String(),
|
||||||
"currentHosting": currentHosting,
|
"userSessionId": userSessionId,
|
||||||
"currentSession": currentSession.isNone || shallow ? null : currentSession.toMap(),
|
"currentSessionIndex": currentSessionIndex,
|
||||||
"activeSessions": shallow ? [] : activeSessions.map((e) => e.toMap(),).toList(),
|
"sessions": shallow
|
||||||
"neosVersion": neosVersion,
|
? []
|
||||||
|
: sessions
|
||||||
|
.map(
|
||||||
|
(e) => e.toMap(),
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
|
"appVersion": appVersion,
|
||||||
"outputDevice": outputDevice,
|
"outputDevice": outputDevice,
|
||||||
"isMobile": isMobile,
|
"isMobile": isMobile,
|
||||||
"compatibilityHash": compatibilityHash,
|
"compatibilityHash": compatibilityHash,
|
||||||
|
@ -73,27 +123,33 @@ class UserStatus {
|
||||||
UserStatus copyWith({
|
UserStatus copyWith({
|
||||||
OnlineStatus? onlineStatus,
|
OnlineStatus? onlineStatus,
|
||||||
DateTime? lastStatusChange,
|
DateTime? lastStatusChange,
|
||||||
int? currentSessionAccessLevel,
|
DateTime? lastPresenceTimestamp,
|
||||||
bool? currentSessionHidden,
|
bool? isPresent,
|
||||||
bool? currentHosting,
|
String? userSessionId,
|
||||||
Session? currentSession,
|
int? currentSessionIndex,
|
||||||
List<Session>? activeSessions,
|
List<SessionMetadata>? sessions,
|
||||||
String? neosVersion,
|
String? appVersion,
|
||||||
String? outputDevice,
|
String? outputDevice,
|
||||||
bool? isMobile,
|
bool? isMobile,
|
||||||
String? compatibilityHash,
|
String? compatibilityHash,
|
||||||
|
String? hashSalt,
|
||||||
|
UserSessionType? sessionType,
|
||||||
|
List<Session>? sessionData,
|
||||||
}) =>
|
}) =>
|
||||||
UserStatus(
|
UserStatus(
|
||||||
onlineStatus: onlineStatus ?? this.onlineStatus,
|
onlineStatus: onlineStatus ?? this.onlineStatus,
|
||||||
lastStatusChange: lastStatusChange ?? this.lastStatusChange,
|
lastStatusChange: lastStatusChange ?? this.lastStatusChange,
|
||||||
currentSessionAccessLevel: currentSessionAccessLevel ?? this.currentSessionAccessLevel,
|
lastPresenceTimestamp: lastPresenceTimestamp ?? this.lastPresenceTimestamp,
|
||||||
currentSessionHidden: currentSessionHidden ?? this.currentSessionHidden,
|
isPresent: isPresent ?? this.isPresent,
|
||||||
currentHosting: currentHosting ?? this.currentHosting,
|
userSessionId: userSessionId ?? this.userSessionId,
|
||||||
currentSession: currentSession ?? this.currentSession,
|
currentSessionIndex: currentSessionIndex ?? this.currentSessionIndex,
|
||||||
activeSessions: activeSessions ?? this.activeSessions,
|
sessions: sessions ?? this.sessions,
|
||||||
neosVersion: neosVersion ?? this.neosVersion,
|
appVersion: appVersion ?? this.appVersion,
|
||||||
outputDevice: outputDevice ?? this.outputDevice,
|
outputDevice: outputDevice ?? this.outputDevice,
|
||||||
isMobile: isMobile ?? this.isMobile,
|
isMobile: isMobile ?? this.isMobile,
|
||||||
compatibilityHash: compatibilityHash ?? this.compatibilityHash,
|
compatibilityHash: compatibilityHash ?? this.compatibilityHash,
|
||||||
|
hashSalt: hashSalt ?? this.hashSalt,
|
||||||
|
sessionType: sessionType ?? this.sessionType,
|
||||||
|
decodedSessions: sessionData ?? this.decodedSessions,
|
||||||
);
|
);
|
||||||
}
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
import 'package:contacts_plus_plus/string_formatter.dart';
|
import 'package:recon/string_formatter.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
class FormattedText extends StatelessWidget {
|
class FormattedText extends StatelessWidget {
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
import 'package:contacts_plus_plus/auxiliary.dart';
|
import 'package:recon/auxiliary.dart';
|
||||||
import 'package:contacts_plus_plus/clients/messaging_client.dart';
|
import 'package:recon/clients/messaging_client.dart';
|
||||||
import 'package:contacts_plus_plus/models/users/friend.dart';
|
import 'package:recon/models/message.dart';
|
||||||
import 'package:contacts_plus_plus/models/message.dart';
|
import 'package:recon/models/users/friend.dart';
|
||||||
import 'package:contacts_plus_plus/widgets/formatted_text.dart';
|
import 'package:recon/widgets/formatted_text.dart';
|
||||||
import 'package:contacts_plus_plus/widgets/friends/friend_online_status_indicator.dart';
|
import 'package:recon/widgets/friends/friend_online_status_indicator.dart';
|
||||||
import 'package:contacts_plus_plus/widgets/generic_avatar.dart';
|
import 'package:recon/widgets/generic_avatar.dart';
|
||||||
import 'package:contacts_plus_plus/widgets/messages/messages_list.dart';
|
import 'package:recon/widgets/messages/messages_list.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
@ -19,8 +19,12 @@ class FriendListTile extends StatelessWidget {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final imageUri = Aux.neosDbToHttp(friend.userProfile.iconUrl);
|
final imageUri = Aux.resdbToHttp(friend.userProfile.iconUrl);
|
||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
|
final mClient = Provider.of<MessagingClient>(context, listen: false);
|
||||||
|
final currentSession = friend.userStatus.currentSessionIndex == -1
|
||||||
|
? null
|
||||||
|
: friend.userStatus.decodedSessions.elementAtOrNull(friend.userStatus.currentSessionIndex);
|
||||||
return ListTile(
|
return ListTile(
|
||||||
leading: GenericAvatar(
|
leading: GenericAvatar(
|
||||||
imageUri: imageUri,
|
imageUri: imageUri,
|
||||||
|
@ -54,11 +58,11 @@ class FriendListTile extends StatelessWidget {
|
||||||
width: 4,
|
width: 4,
|
||||||
),
|
),
|
||||||
Text(toBeginningOfSentenceCase(friend.userStatus.onlineStatus.name) ?? "Unknown"),
|
Text(toBeginningOfSentenceCase(friend.userStatus.onlineStatus.name) ?? "Unknown"),
|
||||||
if (!friend.userStatus.currentSession.isNone) ...[
|
if (currentSession != null && !currentSession.isNone) ...[
|
||||||
const Text(" in "),
|
const Text(" in "),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: FormattedText(
|
child: FormattedText(
|
||||||
friend.userStatus.currentSession.formattedName,
|
currentSession.formattedName,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
))
|
))
|
||||||
|
@ -67,7 +71,6 @@ class FriendListTile extends StatelessWidget {
|
||||||
),
|
),
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
onTap?.call();
|
onTap?.call();
|
||||||
final mClient = Provider.of<MessagingClient>(context, listen: false);
|
|
||||||
mClient.loadUserMessageCache(friend.id);
|
mClient.loadUserMessageCache(friend.id);
|
||||||
final unreads = mClient.getUnreadsForFriend(friend);
|
final unreads = mClient.getUnreadsForFriend(friend);
|
||||||
if (unreads.isNotEmpty) {
|
if (unreads.isNotEmpty) {
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import 'package:contacts_plus_plus/models/users/online_status.dart';
|
import 'package:recon/models/users/online_status.dart';
|
||||||
import 'package:contacts_plus_plus/models/users/user_status.dart';
|
import 'package:recon/models/users/user_status.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
class FriendOnlineStatusIndicator extends StatelessWidget {
|
class FriendOnlineStatusIndicator extends StatelessWidget {
|
||||||
|
@ -9,7 +9,7 @@ class FriendOnlineStatusIndicator extends StatelessWidget {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return userStatus.neosVersion.contains("Contacts++") && userStatus.onlineStatus != OnlineStatus.offline
|
return userStatus.appVersion.contains("ReCon") && userStatus.onlineStatus != OnlineStatus.offline
|
||||||
? SizedBox.square(
|
? SizedBox.square(
|
||||||
dimension: 10,
|
dimension: 10,
|
||||||
child: Image.asset(
|
child: Image.asset(
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import 'package:contacts_plus_plus/clients/messaging_client.dart';
|
import 'package:recon/clients/messaging_client.dart';
|
||||||
import 'package:contacts_plus_plus/widgets/default_error_widget.dart';
|
import 'package:recon/widgets/default_error_widget.dart';
|
||||||
import 'package:contacts_plus_plus/widgets/friends/expanding_input_fab.dart';
|
import 'package:recon/widgets/friends/expanding_input_fab.dart';
|
||||||
import 'package:contacts_plus_plus/widgets/friends/friend_list_tile.dart';
|
import 'package:recon/widgets/friends/friend_list_tile.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
|
|
|
@ -1,11 +1,10 @@
|
||||||
import 'package:contacts_plus_plus/apis/user_api.dart';
|
import 'package:recon/client_holder.dart';
|
||||||
import 'package:contacts_plus_plus/client_holder.dart';
|
import 'package:recon/clients/messaging_client.dart';
|
||||||
import 'package:contacts_plus_plus/clients/messaging_client.dart';
|
import 'package:recon/models/users/online_status.dart';
|
||||||
import 'package:contacts_plus_plus/models/users/online_status.dart';
|
import 'package:recon/widgets/friends/user_search.dart';
|
||||||
import 'package:contacts_plus_plus/models/users/user_status.dart';
|
import 'package:recon/widgets/my_profile_dialog.dart';
|
||||||
import 'package:contacts_plus_plus/widgets/friends/user_search.dart';
|
|
||||||
import 'package:contacts_plus_plus/widgets/my_profile_dialog.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
|
@ -17,133 +16,70 @@ class FriendsListAppBar extends StatefulWidget {
|
||||||
}
|
}
|
||||||
|
|
||||||
class _FriendsListAppBarState extends State<FriendsListAppBar> with AutomaticKeepAliveClientMixin {
|
class _FriendsListAppBarState extends State<FriendsListAppBar> with AutomaticKeepAliveClientMixin {
|
||||||
Future<UserStatus>? _userStatusFuture;
|
|
||||||
ClientHolder? _clientHolder;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void didChangeDependencies() async {
|
|
||||||
super.didChangeDependencies();
|
|
||||||
final clientHolder = ClientHolder.of(context);
|
|
||||||
if (_clientHolder != clientHolder) {
|
|
||||||
_clientHolder = clientHolder;
|
|
||||||
_refreshUserStatus();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void _refreshUserStatus() {
|
|
||||||
final apiClient = _clientHolder!.apiClient;
|
|
||||||
_userStatusFuture ??= UserApi.getUserStatus(apiClient, userId: apiClient.userId).then((value) async {
|
|
||||||
if (value.onlineStatus == OnlineStatus.offline) {
|
|
||||||
final newStatus = value.copyWith(
|
|
||||||
onlineStatus:
|
|
||||||
OnlineStatus.values[_clientHolder!.settingsClient.currentSettings.lastOnlineStatus.valueOrDefault]);
|
|
||||||
await UserApi.setStatus(apiClient, status: newStatus);
|
|
||||||
return newStatus;
|
|
||||||
}
|
|
||||||
return value;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
super.build(context);
|
super.build(context);
|
||||||
return AppBar(
|
return AppBar(
|
||||||
title: const Text("Contacts++"),
|
title: const Text("ReCon"),
|
||||||
|
systemOverlayStyle: SystemUiOverlayStyle(
|
||||||
|
systemNavigationBarColor: Theme.of(context).navigationBarTheme.backgroundColor,
|
||||||
|
),
|
||||||
actions: [
|
actions: [
|
||||||
FutureBuilder(
|
Consumer<MessagingClient>(builder: (context, client, _) {
|
||||||
future: _userStatusFuture,
|
return PopupMenuButton<OnlineStatus>(
|
||||||
builder: (context, snapshot) {
|
child: Row(
|
||||||
if (snapshot.hasData) {
|
children: [
|
||||||
final userStatus = snapshot.data as UserStatus;
|
Padding(
|
||||||
return PopupMenuButton<OnlineStatus>(
|
padding: const EdgeInsets.only(right: 8.0),
|
||||||
child: Row(
|
child: Icon(
|
||||||
children: [
|
Icons.circle,
|
||||||
Padding(
|
size: 16,
|
||||||
padding: const EdgeInsets.only(right: 8.0),
|
color: client.userStatus.onlineStatus.color(context),
|
||||||
child: Icon(
|
),
|
||||||
|
),
|
||||||
|
Text(toBeginningOfSentenceCase(client.userStatus.onlineStatus.name) ?? "Unknown"),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
onSelected: (OnlineStatus onlineStatus) async {
|
||||||
|
final newStatus = client.userStatus.copyWith(onlineStatus: onlineStatus);
|
||||||
|
final settingsClient = ClientHolder.of(context).settingsClient;
|
||||||
|
try {
|
||||||
|
await client.setUserStatus(newStatus);
|
||||||
|
await settingsClient
|
||||||
|
.changeSettings(settingsClient.currentSettings.copyWith(lastOnlineStatus: onlineStatus.index));
|
||||||
|
} catch (e, s) {
|
||||||
|
FlutterError.reportError(FlutterErrorDetails(exception: e, stack: s));
|
||||||
|
if (context.mounted) {
|
||||||
|
ScaffoldMessenger.of(context)
|
||||||
|
.showSnackBar(const SnackBar(content: Text("Failed to set online-status.")));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
itemBuilder: (BuildContext context) => OnlineStatus.values
|
||||||
|
.where((element) => element == OnlineStatus.online || element == OnlineStatus.invisible)
|
||||||
|
.map(
|
||||||
|
(item) => PopupMenuItem<OnlineStatus>(
|
||||||
|
value: item,
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
Icons.circle,
|
Icons.circle,
|
||||||
size: 16,
|
size: 16,
|
||||||
color: userStatus.onlineStatus.color(context),
|
color: item.color(context),
|
||||||
),
|
),
|
||||||
),
|
const SizedBox(
|
||||||
Text(toBeginningOfSentenceCase(userStatus.onlineStatus.name) ?? "Unknown"),
|
width: 8,
|
||||||
],
|
),
|
||||||
),
|
Text(toBeginningOfSentenceCase(item.name)!),
|
||||||
onSelected: (OnlineStatus onlineStatus) async {
|
],
|
||||||
try {
|
|
||||||
final newStatus = userStatus.copyWith(onlineStatus: onlineStatus);
|
|
||||||
setState(() {
|
|
||||||
_userStatusFuture = Future.value(newStatus.copyWith(lastStatusChange: DateTime.now()));
|
|
||||||
});
|
|
||||||
final settingsClient = _clientHolder!.settingsClient;
|
|
||||||
await UserApi.setStatus(_clientHolder!.apiClient, status: newStatus);
|
|
||||||
await settingsClient.changeSettings(
|
|
||||||
settingsClient.currentSettings.copyWith(lastOnlineStatus: onlineStatus.index));
|
|
||||||
} catch (e, s) {
|
|
||||||
FlutterError.reportError(FlutterErrorDetails(exception: e, stack: s));
|
|
||||||
ScaffoldMessenger.of(context)
|
|
||||||
.showSnackBar(const SnackBar(content: Text("Failed to set online-status.")));
|
|
||||||
setState(() {
|
|
||||||
_userStatusFuture = Future.value(userStatus);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
itemBuilder: (BuildContext context) => OnlineStatus.values
|
|
||||||
.where((element) => element == OnlineStatus.online || element == OnlineStatus.invisible)
|
|
||||||
.map(
|
|
||||||
(item) => PopupMenuItem<OnlineStatus>(
|
|
||||||
value: item,
|
|
||||||
child: Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Icon(
|
|
||||||
Icons.circle,
|
|
||||||
size: 16,
|
|
||||||
color: item.color(context),
|
|
||||||
),
|
|
||||||
const SizedBox(
|
|
||||||
width: 8,
|
|
||||||
),
|
|
||||||
Text(toBeginningOfSentenceCase(item.name)!),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
)
|
|
||||||
.toList());
|
|
||||||
} else if (snapshot.hasError) {
|
|
||||||
return TextButton.icon(
|
|
||||||
style: TextButton.styleFrom(
|
|
||||||
foregroundColor: Theme.of(context).colorScheme.onSurface,
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 2)),
|
|
||||||
onPressed: () {
|
|
||||||
setState(() {
|
|
||||||
_userStatusFuture =
|
|
||||||
UserApi.getUserStatus(_clientHolder!.apiClient, userId: _clientHolder!.apiClient.userId);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
icon: const Icon(Icons.warning),
|
|
||||||
label: const Text("Retry"),
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
return TextButton.icon(
|
|
||||||
style: TextButton.styleFrom(
|
|
||||||
disabledForegroundColor: Theme.of(context).colorScheme.onSurface,
|
|
||||||
),
|
|
||||||
onPressed: null,
|
|
||||||
icon: Container(
|
|
||||||
width: 16,
|
|
||||||
height: 16,
|
|
||||||
margin: const EdgeInsets.only(right: 4),
|
|
||||||
child: CircularProgressIndicator(
|
|
||||||
strokeWidth: 2,
|
|
||||||
color: Theme.of(context).colorScheme.onSurface,
|
|
||||||
),
|
),
|
||||||
),
|
)
|
||||||
label: const Text("Loading"),
|
.toList(),
|
||||||
);
|
);
|
||||||
}
|
}),
|
||||||
},
|
|
||||||
),
|
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.only(left: 4, right: 4),
|
padding: const EdgeInsets.only(left: 4, right: 4),
|
||||||
child: PopupMenuButton<MenuItemDefinition>(
|
child: PopupMenuButton<MenuItemDefinition>(
|
||||||
|
@ -182,16 +118,16 @@ class _FriendsListAppBarState extends State<FriendsListAppBar> with AutomaticKee
|
||||||
]
|
]
|
||||||
.map(
|
.map(
|
||||||
(item) => PopupMenuItem<MenuItemDefinition>(
|
(item) => PopupMenuItem<MenuItemDefinition>(
|
||||||
value: item,
|
value: item,
|
||||||
child: Row(
|
child: Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
Text(item.name),
|
Text(item.name),
|
||||||
Icon(item.icon),
|
Icon(item.icon),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.toList(),
|
.toList(),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import 'package:contacts_plus_plus/apis/user_api.dart';
|
import 'package:recon/apis/contact_api.dart';
|
||||||
import 'package:contacts_plus_plus/auxiliary.dart';
|
import 'package:recon/auxiliary.dart';
|
||||||
import 'package:contacts_plus_plus/client_holder.dart';
|
import 'package:recon/client_holder.dart';
|
||||||
import 'package:contacts_plus_plus/models/users/user.dart';
|
import 'package:recon/models/users/user.dart';
|
||||||
import 'package:contacts_plus_plus/widgets/generic_avatar.dart';
|
import 'package:recon/widgets/generic_avatar.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
|
|
||||||
|
@ -41,7 +41,7 @@ class _UserListTileState extends State<UserListTile> {
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
return ListTile(
|
return ListTile(
|
||||||
leading: GenericAvatar(imageUri: Aux.neosDbToHttp(widget.user.userProfile?.iconUrl),),
|
leading: GenericAvatar(imageUri: Aux.resdbToHttp(widget.user.userProfile?.iconUrl),),
|
||||||
title: Text(widget.user.username),
|
title: Text(widget.user.username),
|
||||||
subtitle: Text(_regDateFormat.format(widget.user.registrationDate)),
|
subtitle: Text(_regDateFormat.format(widget.user.registrationDate)),
|
||||||
trailing: IconButton(
|
trailing: IconButton(
|
||||||
|
@ -55,11 +55,11 @@ class _UserListTileState extends State<UserListTile> {
|
||||||
});
|
});
|
||||||
try {
|
try {
|
||||||
if (_localAdded) {
|
if (_localAdded) {
|
||||||
await UserApi.removeUserAsFriend(ClientHolder
|
await ContactApi.removeUserAsFriend(ClientHolder
|
||||||
.of(context)
|
.of(context)
|
||||||
.apiClient, user: widget.user);
|
.apiClient, user: widget.user);
|
||||||
} else {
|
} else {
|
||||||
await UserApi.addUserAsFriend(ClientHolder
|
await ContactApi.addUserAsFriend(ClientHolder
|
||||||
.of(context)
|
.of(context)
|
||||||
.apiClient, user: widget.user);
|
.apiClient, user: widget.user);
|
||||||
}
|
}
|
||||||
|
@ -70,7 +70,8 @@ class _UserListTileState extends State<UserListTile> {
|
||||||
widget.onChanged?.call();
|
widget.onChanged?.call();
|
||||||
} catch (e, s) {
|
} catch (e, s) {
|
||||||
FlutterError.reportError(FlutterErrorDetails(exception: e, stack: s));
|
FlutterError.reportError(FlutterErrorDetails(exception: e, stack: s));
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
if (context.mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(
|
SnackBar(
|
||||||
duration: const Duration(seconds: 5),
|
duration: const Duration(seconds: 5),
|
||||||
content: Text(
|
content: Text(
|
||||||
|
@ -80,6 +81,7 @@ class _UserListTileState extends State<UserListTile> {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
}
|
||||||
setState(() {
|
setState(() {
|
||||||
_loading = false;
|
_loading = false;
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:contacts_plus_plus/apis/user_api.dart';
|
import 'package:recon/apis/user_api.dart';
|
||||||
import 'package:contacts_plus_plus/client_holder.dart';
|
import 'package:recon/client_holder.dart';
|
||||||
import 'package:contacts_plus_plus/clients/messaging_client.dart';
|
import 'package:recon/clients/messaging_client.dart';
|
||||||
import 'package:contacts_plus_plus/models/users/user.dart';
|
import 'package:recon/models/users/user.dart';
|
||||||
import 'package:contacts_plus_plus/widgets/default_error_widget.dart';
|
import 'package:recon/widgets/default_error_widget.dart';
|
||||||
import 'package:contacts_plus_plus/widgets/friends/user_list_tile.dart';
|
import 'package:recon/widgets/friends/user_list_tile.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
import 'package:contacts_plus_plus/widgets/friends/friends_list.dart';
|
import 'package:recon/widgets/friends/friends_list.dart';
|
||||||
import 'package:contacts_plus_plus/widgets/friends/friends_list_app_bar.dart';
|
import 'package:recon/widgets/friends/friends_list_app_bar.dart';
|
||||||
import 'package:contacts_plus_plus/widgets/inventory/inventory_browser.dart';
|
import 'package:recon/widgets/inventory/inventory_browser.dart';
|
||||||
import 'package:contacts_plus_plus/widgets/inventory/inventory_browser_app_bar.dart';
|
import 'package:recon/widgets/inventory/inventory_browser_app_bar.dart';
|
||||||
import 'package:contacts_plus_plus/widgets/sessions/session_list.dart';
|
import 'package:recon/widgets/sessions/session_list.dart';
|
||||||
import 'package:contacts_plus_plus/widgets/sessions/session_list_app_bar.dart';
|
import 'package:recon/widgets/sessions/session_list_app_bar.dart';
|
||||||
import 'package:contacts_plus_plus/widgets/settings_app_bar.dart';
|
import 'package:recon/widgets/settings_app_bar.dart';
|
||||||
import 'package:contacts_plus_plus/widgets/settings_page.dart';
|
import 'package:recon/widgets/settings_page.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
class Home extends StatefulWidget {
|
class Home extends StatefulWidget {
|
||||||
|
@ -29,6 +29,7 @@ class _HomeState extends State<Home> {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
|
backgroundColor: Theme.of(context).colorScheme.surface,
|
||||||
appBar: PreferredSize(
|
appBar: PreferredSize(
|
||||||
preferredSize: const Size.fromHeight(kToolbarHeight),
|
preferredSize: const Size.fromHeight(kToolbarHeight),
|
||||||
child: AnimatedSwitcher(
|
child: AnimatedSwitcher(
|
||||||
|
@ -46,46 +47,36 @@ class _HomeState extends State<Home> {
|
||||||
SettingsPage(),
|
SettingsPage(),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
bottomNavigationBar: Container(
|
bottomNavigationBar: NavigationBar(
|
||||||
decoration: BoxDecoration(
|
selectedIndex: _selectedPage,
|
||||||
border: const Border(top: BorderSide(width: 1, color: Colors.black)),
|
onDestinationSelected: (index) {
|
||||||
color: Theme.of(context).colorScheme.background,
|
_pageController.animateToPage(
|
||||||
),
|
index,
|
||||||
child: BottomNavigationBar(
|
duration: const Duration(milliseconds: 200),
|
||||||
backgroundColor: Theme.of(context).colorScheme.surface,
|
curve: Curves.easeOut,
|
||||||
type: BottomNavigationBarType.fixed,
|
);
|
||||||
unselectedItemColor: Theme.of(context).colorScheme.onBackground,
|
setState(() {
|
||||||
selectedItemColor: Theme.of(context).colorScheme.primary,
|
_selectedPage = index;
|
||||||
currentIndex: _selectedPage,
|
});
|
||||||
onTap: (index) {
|
},
|
||||||
_pageController.animateToPage(
|
destinations: const [
|
||||||
index,
|
NavigationDestination(
|
||||||
duration: const Duration(milliseconds: 200),
|
icon: Icon(Icons.message),
|
||||||
curve: Curves.easeOut,
|
label: "Chat",
|
||||||
);
|
),
|
||||||
setState(() {
|
NavigationDestination(
|
||||||
_selectedPage = index;
|
icon: Icon(Icons.public),
|
||||||
});
|
label: "Sessions",
|
||||||
},
|
),
|
||||||
items: const [
|
NavigationDestination(
|
||||||
BottomNavigationBarItem(
|
icon: Icon(Icons.inventory),
|
||||||
icon: Icon(Icons.message),
|
label: "Inventory",
|
||||||
label: "Chat",
|
),
|
||||||
),
|
NavigationDestination(
|
||||||
BottomNavigationBarItem(
|
icon: Icon(Icons.settings),
|
||||||
icon: Icon(Icons.public),
|
label: "Settings",
|
||||||
label: "Sessions",
|
),
|
||||||
),
|
],
|
||||||
BottomNavigationBarItem(
|
|
||||||
icon: Icon(Icons.inventory),
|
|
||||||
label: "Inventory",
|
|
||||||
),
|
|
||||||
BottomNavigationBarItem(
|
|
||||||
icon: Icon(Icons.settings),
|
|
||||||
label: "Settings",
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,13 +2,13 @@ import 'dart:async';
|
||||||
|
|
||||||
import 'package:cached_network_image/cached_network_image.dart';
|
import 'package:cached_network_image/cached_network_image.dart';
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
import 'package:contacts_plus_plus/auxiliary.dart';
|
import 'package:recon/auxiliary.dart';
|
||||||
import 'package:contacts_plus_plus/clients/inventory_client.dart';
|
import 'package:recon/clients/inventory_client.dart';
|
||||||
import 'package:contacts_plus_plus/models/inventory/neos_path.dart';
|
import 'package:recon/models/inventory/resonite_directory.dart';
|
||||||
import 'package:contacts_plus_plus/models/records/record.dart';
|
import 'package:recon/models/records/record.dart';
|
||||||
import 'package:contacts_plus_plus/widgets/default_error_widget.dart';
|
import 'package:recon/widgets/default_error_widget.dart';
|
||||||
import 'package:contacts_plus_plus/widgets/inventory/object_inventory_tile.dart';
|
import 'package:recon/widgets/inventory/object_inventory_tile.dart';
|
||||||
import 'package:contacts_plus_plus/widgets/inventory/path_inventory_tile.dart';
|
import 'package:recon/widgets/inventory/path_inventory_tile.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:photo_view/photo_view.dart';
|
import 'package:photo_view/photo_view.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
@ -37,7 +37,7 @@ class _InventoryBrowserState extends State<InventoryBrowser> with AutomaticKeepA
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
super.build(context);
|
super.build(context);
|
||||||
return Consumer<InventoryClient>(builder: (BuildContext context, InventoryClient iClient, Widget? child) {
|
return Consumer<InventoryClient>(builder: (BuildContext context, InventoryClient iClient, Widget? child) {
|
||||||
return FutureBuilder<NeosDirectory>(
|
return FutureBuilder<ResoniteDirectory>(
|
||||||
future: iClient.directoryFuture,
|
future: iClient.directoryFuture,
|
||||||
builder: (context, snapshot) {
|
builder: (context, snapshot) {
|
||||||
final currentDir = snapshot.data;
|
final currentDir = snapshot.data;
|
||||||
|
@ -57,7 +57,9 @@ class _InventoryBrowserState extends State<InventoryBrowser> with AutomaticKeepA
|
||||||
await iClient.reloadCurrentDirectory();
|
await iClient.reloadCurrentDirectory();
|
||||||
_refreshLimiter = Timer(_refreshLimit, () {});
|
_refreshLimiter = Timer(_refreshLimit, () {});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text("Refresh failed: $e")));
|
if (context.mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text("Refresh failed: $e")));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
child: Builder(
|
child: Builder(
|
||||||
|
@ -179,7 +181,7 @@ class _InventoryBrowserState extends State<InventoryBrowser> with AutomaticKeepA
|
||||||
builder: (context) => PhotoView(
|
builder: (context) => PhotoView(
|
||||||
minScale: PhotoViewComputedScale.contained,
|
minScale: PhotoViewComputedScale.contained,
|
||||||
imageProvider:
|
imageProvider:
|
||||||
CachedNetworkImageProvider(Aux.neosDbToHttp(record.thumbnailUri)),
|
CachedNetworkImageProvider(Aux.resdbToHttp(record.thumbnailUri)),
|
||||||
heroAttributes: PhotoViewHeroAttributes(tag: record.id),
|
heroAttributes: PhotoViewHeroAttributes(tag: record.id),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
@ -1,10 +1,11 @@
|
||||||
import 'dart:isolate';
|
import 'dart:isolate';
|
||||||
import 'dart:ui';
|
import 'dart:ui';
|
||||||
|
|
||||||
import 'package:contacts_plus_plus/auxiliary.dart';
|
import 'package:recon/auxiliary.dart';
|
||||||
import 'package:contacts_plus_plus/clients/inventory_client.dart';
|
import 'package:recon/clients/inventory_client.dart';
|
||||||
import 'package:file_picker/file_picker.dart';
|
import 'package:file_picker/file_picker.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter_downloader/flutter_downloader.dart';
|
import 'package:flutter_downloader/flutter_downloader.dart';
|
||||||
import 'package:path/path.dart';
|
import 'package:path/path.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
@ -60,10 +61,16 @@ class _InventoryBrowserAppBarState extends State<InventoryBrowserAppBar> {
|
||||||
? AppBar(
|
? AppBar(
|
||||||
key: const ValueKey("default-appbar"),
|
key: const ValueKey("default-appbar"),
|
||||||
title: const Text("Inventory"),
|
title: const Text("Inventory"),
|
||||||
|
systemOverlayStyle: SystemUiOverlayStyle(
|
||||||
|
systemNavigationBarColor: Theme.of(context).navigationBarTheme.backgroundColor,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
: AppBar(
|
: AppBar(
|
||||||
key: const ValueKey("selection-appbar"),
|
key: const ValueKey("selection-appbar"),
|
||||||
title: Text("${iClient.selectedRecordCount} Selected"),
|
title: Text("${iClient.selectedRecordCount} Selected"),
|
||||||
|
systemOverlayStyle: SystemUiOverlayStyle(
|
||||||
|
systemNavigationBarColor: Theme.of(context).navigationBarTheme.backgroundColor,
|
||||||
|
),
|
||||||
leading: IconButton(
|
leading: IconButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
iClient.clearSelectedRecords();
|
iClient.clearSelectedRecords();
|
||||||
|
@ -142,7 +149,7 @@ class _InventoryBrowserAppBarState extends State<InventoryBrowserAppBar> {
|
||||||
for (var record in selectedRecords) {
|
for (var record in selectedRecords) {
|
||||||
final uri = selectedUris == thumbUris ? record.thumbnailUri : record.thumbnailUri;
|
final uri = selectedUris == thumbUris ? record.thumbnailUri : record.thumbnailUri;
|
||||||
await FlutterDownloader.enqueue(
|
await FlutterDownloader.enqueue(
|
||||||
url: Aux.neosDbToHttp(uri),
|
url: Aux.resdbToHttp(uri),
|
||||||
savedDir: directory,
|
savedDir: directory,
|
||||||
showNotification: true,
|
showNotification: true,
|
||||||
openFileFromNotification: false,
|
openFileFromNotification: false,
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import 'package:cached_network_image/cached_network_image.dart';
|
import 'package:cached_network_image/cached_network_image.dart';
|
||||||
import 'package:contacts_plus_plus/auxiliary.dart';
|
import 'package:recon/auxiliary.dart';
|
||||||
import 'package:contacts_plus_plus/models/records/record.dart';
|
import 'package:recon/models/records/record.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
|
|
||||||
|
@ -42,7 +42,7 @@ class ObjectInventoryTile extends StatelessWidget {
|
||||||
child: CachedNetworkImage(
|
child: CachedNetworkImage(
|
||||||
height: double.infinity,
|
height: double.infinity,
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
imageUrl: Aux.neosDbToHttp(record.thumbnailUri),
|
imageUrl: Aux.resdbToHttp(record.thumbnailUri),
|
||||||
fit: BoxFit.cover,
|
fit: BoxFit.cover,
|
||||||
errorWidget: (context, url, error) => const Center(
|
errorWidget: (context, url, error) => const Center(
|
||||||
child: Icon(
|
child: Icon(
|
||||||
|
|