mirror of
https://github.com/twonlyapp/twonly-app.git
synced 2026-01-15 11:18:41 +00:00
websocket works
This commit is contained in:
parent
3b3c5e61b9
commit
e358cf2e57
5 changed files with 108 additions and 209 deletions
|
|
@ -1,5 +1,4 @@
|
|||
import 'package:camera/camera.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:twonly/src/providers/api_provider.dart';
|
||||
import 'package:twonly/src/providers/db_provider.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
|
@ -10,6 +9,7 @@ import 'src/settings/settings_controller.dart';
|
|||
import 'src/settings/settings_service.dart';
|
||||
|
||||
late DbProvider dbProvider;
|
||||
late ApiProvider apiProvider;
|
||||
|
||||
void main() async {
|
||||
// Set up the SettingsController, which will glue user settings to multiple
|
||||
|
|
@ -27,7 +27,7 @@ void main() async {
|
|||
// check if release build or debug build
|
||||
final kDebugMode = true;
|
||||
|
||||
Logger.root.level = Level.FINEST; // defaults to Level.INFO
|
||||
Logger.root.level = Level.ALL; // defaults to Level.INFO
|
||||
Logger.root.onRecord.listen((record) {
|
||||
// if (kDebugMode) {
|
||||
// ignore: avoid_print
|
||||
|
|
@ -36,7 +36,7 @@ void main() async {
|
|||
});
|
||||
|
||||
var cameras = await availableCameras();
|
||||
t
|
||||
|
||||
// Create or open the database
|
||||
dbProvider = DbProvider();
|
||||
await dbProvider.ready;
|
||||
|
|
@ -45,16 +45,22 @@ void main() async {
|
|||
var apiUrl = "ws://api.theconnectapp.de/v-1/";
|
||||
if (true) {
|
||||
// Overwrite the domain in your local network so you can test the app locally
|
||||
apiUrl = "ws://9.99.0.6:3030/api/client";
|
||||
apiUrl = "ws://10.99.0.6:3030/api/client";
|
||||
}
|
||||
|
||||
apiProvider = ApiProvider(apiUrl: apiUrl, backupApiUrl: null);
|
||||
|
||||
// TODO: Open the connection in the background so the app launch is not delayed.
|
||||
//await apiProvider.connect();
|
||||
|
||||
// Workmanager.executeTask((task, inputData) async {
|
||||
// await _HomeState().manager();
|
||||
// print('Background Services are Working!');//This is Working
|
||||
// return true;
|
||||
// });
|
||||
|
||||
runApp(ChangeNotifierProvider<ApiProvider>(
|
||||
child: MyApp(settingsController: settingsController, cameras: cameras),
|
||||
create: (_) => ApiProvider(apiUrl: apiUrl, backupApiUrl: apiUrl)));
|
||||
// Run the app and pass in the SettingsController. The app listens to the
|
||||
// SettingsController for changes, then passes it further down to the
|
||||
// SettingsView.
|
||||
runApp(MyApp(settingsController: settingsController, cameras: cameras));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import 'package:camera/camera.dart';
|
||||
import 'package:path/path.dart';
|
||||
import 'package:twonly/main.dart';
|
||||
import 'package:twonly/src/providers/api_provider.dart';
|
||||
import 'views/home_view.dart';
|
||||
import 'views/register_view.dart';
|
||||
|
|
@ -26,6 +27,7 @@ class MyApp extends StatefulWidget {
|
|||
|
||||
class _MyAppState extends State<MyApp> {
|
||||
Future<bool> _isUserCreated = isUserCreated();
|
||||
bool _isConnected = false;
|
||||
int redColorOpacity = 0; // Start with dark red
|
||||
bool redColorGoUp = true;
|
||||
bool isConnected = false;
|
||||
|
|
@ -35,6 +37,11 @@ class _MyAppState extends State<MyApp> {
|
|||
super.initState();
|
||||
// Start the color animation
|
||||
_startColorAnimation();
|
||||
apiProvider.connect((isConnected) {
|
||||
setState(() {
|
||||
_isConnected = isConnected;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
void _startColorAnimation() {
|
||||
|
|
@ -60,7 +67,7 @@ class _MyAppState extends State<MyApp> {
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
double screenWidth = MediaQuery.of(context).size.width;
|
||||
var isConnected = context.watch<ApiProvider>().isConnected;
|
||||
// var isConnected = context.watch<ApiProvider>().isConnected;
|
||||
// Glue the SettingsController to the MaterialApp.
|
||||
//
|
||||
// The ListenableBuilder Widget listens to the SettingsController for changes.
|
||||
|
|
@ -97,16 +104,12 @@ class _MyAppState extends State<MyApp> {
|
|||
home: Stack(
|
||||
children: [
|
||||
FutureBuilder<bool>(
|
||||
future: context.watch<ApiProvider>().startBackend(),
|
||||
builder: (context, snapshot) {
|
||||
return FutureBuilder<bool>(
|
||||
future: _isUserCreated,
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.hasData) {
|
||||
return snapshot.data!
|
||||
? HomeView(
|
||||
settingsController:
|
||||
widget.settingsController,
|
||||
settingsController: widget.settingsController,
|
||||
cameras: widget.cameras)
|
||||
: RegisterView(
|
||||
callbackOnSuccess: () {
|
||||
|
|
@ -117,9 +120,8 @@ class _MyAppState extends State<MyApp> {
|
|||
} else {
|
||||
return Container();
|
||||
}
|
||||
});
|
||||
}),
|
||||
if (!isConnected)
|
||||
if (!_isConnected)
|
||||
Positioned(
|
||||
top: 3, // Position it at the top
|
||||
left: (screenWidth * 0.5) / 2, // Center it horizontally
|
||||
|
|
|
|||
|
|
@ -1,17 +1,15 @@
|
|||
import 'dart:collection';
|
||||
import 'dart:convert';
|
||||
import 'dart:ffi';
|
||||
import 'dart:isolate';
|
||||
import 'dart:math';
|
||||
import 'dart:typed_data';
|
||||
import 'package:fixnum/fixnum.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:twonly/src/proto/api/client_to_server.pb.dart' as c;
|
||||
import 'package:twonly/src/proto/api/client_to_server.pb.dart';
|
||||
import 'package:twonly/src/proto/api/error.pb.dart';
|
||||
import 'package:twonly/src/proto/api/server_to_client.pb.dart' as server;
|
||||
import 'package:twonly/src/proto/api/server_to_client.pbserver.dart';
|
||||
import 'package:twonly/src/signal/signal_helper.dart';
|
||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||
import 'package:web_socket_channel/web_socket_channel.dart';
|
||||
|
|
@ -27,170 +25,13 @@ class Result<T, E> {
|
|||
Result.error(this.error) : value = null;
|
||||
}
|
||||
|
||||
enum ExchangeKind { connectionStateChange, sendRequestV0 }
|
||||
|
||||
class Exchange {
|
||||
Exchange({required this.kind, required this.body, required this.seq});
|
||||
final int seq;
|
||||
final ExchangeKind kind;
|
||||
final dynamic body;
|
||||
}
|
||||
|
||||
c.ClientToServer createClientToServerFromHandshake(c.Handshake handshake) {
|
||||
// Create the V0 message
|
||||
var v0 = c.V0()
|
||||
..seq = Int64(0) // You can set this to the appropriate sequence number
|
||||
..handshake = handshake;
|
||||
|
||||
// Create the ClientToServer message
|
||||
var clientToServer = c.ClientToServer()..v0 = v0;
|
||||
|
||||
return clientToServer;
|
||||
}
|
||||
|
||||
Result asResult(server.ServerToClient msg) {
|
||||
if (msg.v0.response.hasOk()) {
|
||||
return Result.success(msg.v0.response.ok);
|
||||
} else {
|
||||
return Result.error(msg.v0.response.error);
|
||||
}
|
||||
}
|
||||
|
||||
class BackendIsolatedArgs {
|
||||
BackendIsolatedArgs(
|
||||
{required this.apiUrl,
|
||||
required this.backupApiUrl,
|
||||
required this.receivePort,
|
||||
required this.sendPort});
|
||||
final String apiUrl;
|
||||
final String backupApiUrl;
|
||||
final ReceivePort receivePort;
|
||||
final SendPort sendPort;
|
||||
}
|
||||
|
||||
class ApiProvider with ChangeNotifier {
|
||||
class ApiProvider {
|
||||
ApiProvider({required this.apiUrl, required this.backupApiUrl});
|
||||
|
||||
final String apiUrl;
|
||||
final String backupApiUrl;
|
||||
bool _isConnected = false;
|
||||
final Map<int, ServerToClient> _sendRequestV0Resp = HashMap();
|
||||
final log = Logger("twonly::ApiProvider");
|
||||
|
||||
final ReceivePort _receivePort = ReceivePort();
|
||||
SendPort? _sendPort;
|
||||
|
||||
Future<ServerToClient?> _waitForResponse(int seq) async {
|
||||
final startTime = DateTime.now();
|
||||
|
||||
final timeout = Duration(seconds: 5);
|
||||
|
||||
while (true) {
|
||||
if (_sendRequestV0Resp[seq] != null) {
|
||||
final tmp = _sendRequestV0Resp[seq];
|
||||
_sendRequestV0Resp.remove(seq);
|
||||
return tmp;
|
||||
}
|
||||
if (DateTime.now().difference(startTime) > timeout) {
|
||||
log.shout("Timeout for message $seq");
|
||||
return null;
|
||||
}
|
||||
await Future.delayed(Duration(milliseconds: 10));
|
||||
}
|
||||
}
|
||||
|
||||
static void _startBackendIsolated(BackendIsolatedArgs args) async {
|
||||
final backend =
|
||||
Backend(apiUrl: args.apiUrl, backupApiUrl: args.backupApiUrl);
|
||||
|
||||
await backend.connect();
|
||||
|
||||
args.receivePort.listen((msg) async {
|
||||
switch (msg.kind) {
|
||||
case ExchangeKind.sendRequestV0:
|
||||
final resp = await backend._sendRequestV0(msg.body);
|
||||
args.sendPort.send(Exchange(
|
||||
kind: ExchangeKind.sendRequestV0, body: resp, seq: msg.seq));
|
||||
break;
|
||||
default:
|
||||
}
|
||||
});
|
||||
args.sendPort.send(
|
||||
Exchange(kind: ExchangeKind.connectionStateChange, body: true, seq: 0));
|
||||
}
|
||||
|
||||
Future<bool> startBackend() async {
|
||||
ReceivePort port = ReceivePort();
|
||||
_sendPort = port.sendPort;
|
||||
|
||||
await Isolate.spawn(
|
||||
_startBackendIsolated,
|
||||
BackendIsolatedArgs(
|
||||
sendPort: _receivePort.sendPort,
|
||||
receivePort: port,
|
||||
apiUrl: apiUrl,
|
||||
backupApiUrl: backupApiUrl));
|
||||
|
||||
_receivePort.listen((msg) {
|
||||
switch (msg.kind) {
|
||||
case ExchangeKind.sendRequestV0:
|
||||
_sendRequestV0Resp[msg.seq] = msg.body;
|
||||
// final resp = await backend._sendRequestV0(msg.body);
|
||||
// args.sendPort.send(Exchange(
|
||||
// kind: ExchangeKind.sendRequestV0, body: resp, seq: msg.seq));
|
||||
break;
|
||||
case ExchangeKind.connectionStateChange:
|
||||
_isConnected = msg.body;
|
||||
default:
|
||||
}
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
bool get isConnected => _isConnected;
|
||||
|
||||
Future<Result> register(String username, String? inviteCode) async {
|
||||
if (_sendPort == null) return Result.error("Unknown error");
|
||||
final reqSignal = await SignalHelper.getRegisterData();
|
||||
|
||||
if (reqSignal == null) {
|
||||
return Result.error(
|
||||
"There was an fatal error. Try reinstalling the app.");
|
||||
}
|
||||
|
||||
var register = c.Handshake_Register()
|
||||
..username = username
|
||||
..publicIdentityKey = reqSignal["identityKey"]
|
||||
..signedPrekey = reqSignal["signedPreKey"]?["key"]
|
||||
..signedPrekeySignature = reqSignal["signedPreKey"]?["signature"]
|
||||
..signedPrekeyId = Int64(reqSignal["signedPreKey"]?["id"]);
|
||||
|
||||
if (inviteCode != null && inviteCode != "") {
|
||||
register.inviteCode = inviteCode;
|
||||
}
|
||||
// Create the Handshake message
|
||||
var handshake = c.Handshake()..register = register;
|
||||
var req = createClientToServerFromHandshake(handshake);
|
||||
|
||||
var seq = Random().nextInt(4294967296);
|
||||
final tmp = Exchange(seq: seq, kind: ExchangeKind.sendRequestV0, body: req);
|
||||
_sendPort!.send(tmp);
|
||||
|
||||
final resp = await _waitForResponse(seq);
|
||||
|
||||
if (resp == null) {
|
||||
return Result.error("Server is not reachable!");
|
||||
}
|
||||
return asResult(resp);
|
||||
}
|
||||
}
|
||||
|
||||
class Backend {
|
||||
Backend({required this.apiUrl, required this.backupApiUrl});
|
||||
|
||||
final String apiUrl;
|
||||
final String? backupApiUrl;
|
||||
final log = Logger("twonly::backend");
|
||||
final log = Logger("connect::ApiProvider");
|
||||
final HashMap<String, List<Function>> _callbacks = HashMap();
|
||||
|
||||
final HashMap<Int64, server.ServerToClient?> messagesV0 = HashMap();
|
||||
|
||||
|
|
@ -213,18 +54,20 @@ class Backend {
|
|||
}
|
||||
}
|
||||
|
||||
Future<bool> connect() async {
|
||||
Future<bool> connect(Function(bool)? callBack) async {
|
||||
print("Trying to connect to the backend $apiUrl!");
|
||||
if (_channel != null && _channel!.closeCode != null) {
|
||||
return true;
|
||||
}
|
||||
log.info("Trying to connect to the backend $apiUrl!");
|
||||
if (await _connectTo(apiUrl)) {
|
||||
if (callBack != null) callBack(true);
|
||||
return true;
|
||||
}
|
||||
if (backupApiUrl != null) {
|
||||
log.info("Trying to connect to the backup backend $backupApiUrl!");
|
||||
if (await _connectTo(backupApiUrl!)) {
|
||||
if (callBack != null) callBack(true);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
@ -246,6 +89,13 @@ class Backend {
|
|||
}
|
||||
}
|
||||
|
||||
void addNotifier(String messageType, Function callBackFunction) {
|
||||
if (!_callbacks.containsKey(messageType)) {
|
||||
_callbacks[messageType] = [];
|
||||
}
|
||||
_callbacks[messageType]!.add(callBackFunction);
|
||||
}
|
||||
|
||||
// TODO: There must be a smarter move to do that :/
|
||||
Future<server.ServerToClient?> _waitForResponse(Int64 seq) async {
|
||||
final startTime = DateTime.now();
|
||||
|
|
@ -266,8 +116,7 @@ class Backend {
|
|||
}
|
||||
}
|
||||
|
||||
Future<server.ServerToClient?> _sendRequestV0(
|
||||
c.ClientToServer request) async {
|
||||
Future<server.ServerToClient?> _sendRequestV0(ClientToServer request) async {
|
||||
var seq = Int64(Random().nextInt(4294967296));
|
||||
while (messagesV0.containsKey(seq)) {
|
||||
seq = Int64(Random().nextInt(4294967296));
|
||||
|
|
@ -278,7 +127,7 @@ class Backend {
|
|||
|
||||
log.info("Check if is connected?");
|
||||
// check if it is connected to the backend. if not try to reconnect.
|
||||
if (!await connect()) {
|
||||
if (!await connect(null)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
@ -287,6 +136,18 @@ class Backend {
|
|||
return await _waitForResponse(seq);
|
||||
}
|
||||
|
||||
ClientToServer createClientToServerFromHandshake(Handshake handshake) {
|
||||
// Create the V0 message
|
||||
var v0 = V0()
|
||||
..seq = Int64(0) // You can set this to the appropriate sequence number
|
||||
..handshake = handshake;
|
||||
|
||||
// Create the ClientToServer message
|
||||
var clientToServer = ClientToServer()..v0 = v0;
|
||||
|
||||
return clientToServer;
|
||||
}
|
||||
|
||||
static String getLocalizedString(BuildContext context, ErrorCode code) {
|
||||
switch (code.toString()) {
|
||||
case "Unknown":
|
||||
|
|
@ -319,4 +180,41 @@ class Backend {
|
|||
return code.toString(); // Fallback for unrecognized keys
|
||||
}
|
||||
}
|
||||
|
||||
Result _asResult(server.ServerToClient msg) {
|
||||
if (msg.v0.response.hasOk()) {
|
||||
return Result.success(msg.v0.response.ok);
|
||||
} else {
|
||||
return Result.error(msg.v0.response.error);
|
||||
}
|
||||
}
|
||||
|
||||
Future<Result> register(String username, String? inviteCode) async {
|
||||
final reqSignal = await SignalHelper.getRegisterData();
|
||||
|
||||
if (reqSignal == null) {
|
||||
return Result.error(
|
||||
"There was an fatal error. Try reinstalling the app.");
|
||||
}
|
||||
|
||||
var register = Handshake_Register()
|
||||
..username = username
|
||||
..publicIdentityKey = reqSignal["identityKey"]
|
||||
..signedPrekey = reqSignal["signedPreKey"]?["key"]
|
||||
..signedPrekeySignature = reqSignal["signedPreKey"]?["signature"]
|
||||
..signedPrekeyId = Int64(reqSignal["signedPreKey"]?["id"]);
|
||||
|
||||
if (inviteCode != null && inviteCode != "") {
|
||||
register.inviteCode = inviteCode;
|
||||
}
|
||||
// Create the Handshake message
|
||||
var handshake = Handshake()..register = register;
|
||||
var req = createClientToServerFromHandshake(handshake);
|
||||
|
||||
final resp = await _sendRequestV0(req);
|
||||
if (resp == null) {
|
||||
return Result.error("Server is not reachable!");
|
||||
}
|
||||
return _asResult(resp);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,9 @@
|
|||
import 'dart:convert';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:twonly/main.dart';
|
||||
import 'package:twonly/src/signal/signal_helper.dart';
|
||||
import 'package:twonly/src/providers/api_provider.dart';
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
import 'model/user_data_json.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
|
||||
// Just a helper function to get the secure storage
|
||||
|
|
@ -46,18 +44,13 @@ Future<bool> deleteLocalUserData() async {
|
|||
return true;
|
||||
}
|
||||
|
||||
Future<Result> createNewUser(
|
||||
BuildContext context, String username, String inviteCode) async {
|
||||
Future<Result> createNewUser(String username, String inviteCode) async {
|
||||
final storage = getSecureStorage();
|
||||
if (!context.mounted) {
|
||||
return Result.error("not mounted");
|
||||
}
|
||||
|
||||
await SignalHelper.createIfNotExistsSignalIdentity();
|
||||
|
||||
// TODO: API call to server to check username and inviteCode
|
||||
// final res = await apiProvider.register(username, inviteCode);
|
||||
final res = await context.watch<ApiProvider>().register(username, inviteCode);
|
||||
final res = await apiProvider.register(username, inviteCode);
|
||||
|
||||
if (res.isSuccess) {
|
||||
print("Got user_id ${res.value}");
|
||||
|
|
|
|||
|
|
@ -120,7 +120,7 @@ class _RegisterViewState extends State<RegisterView> {
|
|||
setState(() {
|
||||
_isTryingToRegister = true;
|
||||
});
|
||||
final res = await createNewUser(context,
|
||||
final res = await createNewUser(
|
||||
usernameController.text, inviteCodeController.text);
|
||||
setState(() {
|
||||
_isTryingToRegister = false;
|
||||
|
|
@ -130,7 +130,7 @@ class _RegisterViewState extends State<RegisterView> {
|
|||
return;
|
||||
}
|
||||
final errMsg =
|
||||
Backend.getLocalizedString(context, res.error);
|
||||
ApiProvider.getLocalizedString(context, res.error);
|
||||
showAlertDialog(context, "Oh no!", errMsg);
|
||||
},
|
||||
style: ButtonStyle(
|
||||
|
|
|
|||
Loading…
Reference in a new issue