mirror of
https://github.com/twonlyapp/twonly-app.git
synced 2026-01-15 15:48:41 +00:00
authentication
This commit is contained in:
parent
aa608be1e4
commit
7fd33ab697
16 changed files with 522 additions and 319 deletions
|
|
@ -24,15 +24,10 @@ void main() async {
|
||||||
// can be called before `runApp()`
|
// can be called before `runApp()`
|
||||||
WidgetsFlutterBinding.ensureInitialized();
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
|
|
||||||
// check if release build or debug build
|
|
||||||
final kDebugMode = true;
|
|
||||||
|
|
||||||
Logger.root.level = Level.ALL; // defaults to Level.INFO
|
Logger.root.level = Level.ALL; // defaults to Level.INFO
|
||||||
Logger.root.onRecord.listen((record) {
|
Logger.root.onRecord.listen((record) {
|
||||||
// if (kDebugMode) {
|
debugPrint(
|
||||||
// ignore: avoid_print
|
'${record.level.name}: twonly:${record.loggerName}: ${record.message}');
|
||||||
print('${record.level.name}: ${record.time}: ${record.message}');
|
|
||||||
// }
|
|
||||||
});
|
});
|
||||||
|
|
||||||
var cameras = await availableCameras();
|
var cameras = await availableCameras();
|
||||||
|
|
@ -50,9 +45,6 @@ void main() async {
|
||||||
|
|
||||||
apiProvider = ApiProvider(apiUrl: apiUrl, backupApiUrl: null);
|
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 {
|
// Workmanager.executeTask((task, inputData) async {
|
||||||
// await _HomeState().manager();
|
// await _HomeState().manager();
|
||||||
// print('Background Services are Working!');//This is Working
|
// print('Background Services are Working!');//This is Working
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,11 @@
|
||||||
import 'package:camera/camera.dart';
|
import 'package:camera/camera.dart';
|
||||||
import 'package:path/path.dart';
|
|
||||||
import 'package:twonly/main.dart';
|
import 'package:twonly/main.dart';
|
||||||
import 'package:twonly/src/providers/api_provider.dart';
|
|
||||||
import 'views/home_view.dart';
|
import 'views/home_view.dart';
|
||||||
import 'views/register_view.dart';
|
import 'views/register_view.dart';
|
||||||
import 'utils.dart';
|
import 'utils.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||||
import 'package:flutter_localizations/flutter_localizations.dart';
|
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||||
import 'package:provider/provider.dart';
|
|
||||||
import 'dart:isolate';
|
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'settings/settings_controller.dart';
|
import 'settings/settings_controller.dart';
|
||||||
|
|
||||||
|
|
@ -37,11 +33,12 @@ class _MyAppState extends State<MyApp> {
|
||||||
super.initState();
|
super.initState();
|
||||||
// Start the color animation
|
// Start the color animation
|
||||||
_startColorAnimation();
|
_startColorAnimation();
|
||||||
apiProvider.connect((isConnected) {
|
apiProvider.setConnectionStateCallback((isConnected) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_isConnected = isConnected;
|
_isConnected = isConnected;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
apiProvider.connect();
|
||||||
}
|
}
|
||||||
|
|
||||||
void _startColorAnimation() {
|
void _startColorAnimation() {
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,10 @@
|
||||||
"registerUsernameDecoration": "Username",
|
"registerUsernameDecoration": "Username",
|
||||||
"registerUsernameLimits": "Username must be 4 to 12 characters long, consisting only of letters (a-z) and numbers (0-9).",
|
"registerUsernameLimits": "Username must be 4 to 12 characters long, consisting only of letters (a-z) and numbers (0-9).",
|
||||||
"registerSubmitButton": "Register now!",
|
"registerSubmitButton": "Register now!",
|
||||||
|
"newMessageTitle": "New message",
|
||||||
|
"chatsTitle": "Chats",
|
||||||
|
"searchUsernameInput": "Username",
|
||||||
|
"searchUsernameTitle": "Search username",
|
||||||
"errorUnknown": "An unexpected error has occurred. Please try again later.",
|
"errorUnknown": "An unexpected error has occurred. Please try again later.",
|
||||||
"errorBadRequest": "The request could not be understood by the server due to malformed syntax. Please check your input and try again.",
|
"errorBadRequest": "The request could not be understood by the server due to malformed syntax. Please check your input and try again.",
|
||||||
"errorTooManyRequests": "You have made too many requests in a short period. Please wait a moment before trying again.",
|
"errorTooManyRequests": "You have made too many requests in a short period. Please wait a moment before trying again.",
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,5 @@
|
||||||
import 'dart:collection';
|
import 'dart:collection';
|
||||||
import 'dart:convert';
|
|
||||||
import 'dart:ffi';
|
|
||||||
import 'dart:math';
|
import 'dart:math';
|
||||||
import 'dart:typed_data';
|
|
||||||
import 'package:fixnum/fixnum.dart';
|
import 'package:fixnum/fixnum.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
|
@ -12,7 +9,9 @@ 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.pb.dart' as server;
|
||||||
import 'package:twonly/src/signal/signal_helper.dart';
|
import 'package:twonly/src/signal/signal_helper.dart';
|
||||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||||
|
import 'package:twonly/src/utils.dart';
|
||||||
import 'package:web_socket_channel/io.dart';
|
import 'package:web_socket_channel/io.dart';
|
||||||
|
import 'package:libsignal_protocol_dart/src/ecc/ed25519.dart';
|
||||||
import 'package:web_socket_channel/web_socket_channel.dart';
|
import 'package:web_socket_channel/web_socket_channel.dart';
|
||||||
|
|
||||||
class Result<T, E> {
|
class Result<T, E> {
|
||||||
|
|
@ -31,7 +30,8 @@ class ApiProvider {
|
||||||
|
|
||||||
final String apiUrl;
|
final String apiUrl;
|
||||||
final String? backupApiUrl;
|
final String? backupApiUrl;
|
||||||
final log = Logger("connect::ApiProvider");
|
int _reconnectionDelay = 5;
|
||||||
|
final log = Logger("api_provider");
|
||||||
Function(bool)? _connectionStateCallback;
|
Function(bool)? _connectionStateCallback;
|
||||||
|
|
||||||
final HashMap<Int64, server.ServerToClient?> messagesV0 = HashMap();
|
final HashMap<Int64, server.ServerToClient?> messagesV0 = HashMap();
|
||||||
|
|
@ -47,7 +47,6 @@ class ApiProvider {
|
||||||
_channel!.stream.listen(_onData, onDone: _onDone, onError: _onError);
|
_channel!.stream.listen(_onData, onDone: _onDone, onError: _onError);
|
||||||
await _channel!.ready;
|
await _channel!.ready;
|
||||||
log.info("Websocket is connected!");
|
log.info("Websocket is connected!");
|
||||||
print("Websocket is connected!");
|
|
||||||
return true;
|
return true;
|
||||||
} on WebSocketChannelException catch (e) {
|
} on WebSocketChannelException catch (e) {
|
||||||
log.shout("Error: $e");
|
log.shout("Error: $e");
|
||||||
|
|
@ -55,25 +54,24 @@ class ApiProvider {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> connect(Function(bool)? callBack) async {
|
Future<bool> connect() async {
|
||||||
print("Trying to connect to the backend $apiUrl!");
|
|
||||||
if (callBack != null) {
|
|
||||||
_connectionStateCallback = callBack;
|
|
||||||
}
|
|
||||||
if (_channel != null && _channel!.closeCode != null) {
|
if (_channel != null && _channel!.closeCode != null) {
|
||||||
print("is connected");
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
log.info("Trying to connect to the backend $apiUrl!");
|
log.info("Trying to connect to the backend $apiUrl!");
|
||||||
if (await _connectTo(apiUrl)) {
|
if (await _connectTo(apiUrl)) {
|
||||||
if (callBack != null) callBack(true);
|
await authenticate();
|
||||||
|
if (_connectionStateCallback != null) _connectionStateCallback!(true);
|
||||||
|
_reconnectionDelay = 5;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (backupApiUrl != null) {
|
if (backupApiUrl != null) {
|
||||||
log.info("Trying to connect to the backup backend $backupApiUrl!");
|
log.info("Trying to connect to the backup backend $backupApiUrl!");
|
||||||
if (await _connectTo(backupApiUrl!)) {
|
if (await _connectTo(backupApiUrl!)) {
|
||||||
if (callBack != null) callBack(true);
|
await authenticate();
|
||||||
|
if (_connectionStateCallback != null) _connectionStateCallback!(true);
|
||||||
|
_reconnectionDelay = 5;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -87,7 +85,7 @@ class ApiProvider {
|
||||||
_connectionStateCallback!(false);
|
_connectionStateCallback!(false);
|
||||||
}
|
}
|
||||||
_channel = null;
|
_channel = null;
|
||||||
tryToReconnect(5);
|
tryToReconnect();
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onError(dynamic e) {
|
void _onError(dynamic e) {
|
||||||
|
|
@ -95,20 +93,21 @@ class ApiProvider {
|
||||||
_connectionStateCallback!(false);
|
_connectionStateCallback!(false);
|
||||||
}
|
}
|
||||||
_channel = null;
|
_channel = null;
|
||||||
tryToReconnect(5);
|
tryToReconnect();
|
||||||
}
|
}
|
||||||
|
|
||||||
void tryToReconnect(int delay) {
|
void setConnectionStateCallback(Function(bool) callBack) {
|
||||||
Future.delayed(Duration(seconds: delay)).then(
|
_connectionStateCallback = callBack;
|
||||||
|
}
|
||||||
|
|
||||||
|
void tryToReconnect() {
|
||||||
|
Future.delayed(Duration(seconds: _reconnectionDelay)).then(
|
||||||
(value) async {
|
(value) async {
|
||||||
if (!await connect(_connectionStateCallback)) {
|
_reconnectionDelay = _reconnectionDelay * 2;
|
||||||
if (delay > 60 * 5) {
|
if (_reconnectionDelay > 60 * 5) {
|
||||||
delay = 60 * 5;
|
_reconnectionDelay = 60 * 5;
|
||||||
} else {
|
|
||||||
delay = delay * 2;
|
|
||||||
}
|
|
||||||
tryToReconnect(delay);
|
|
||||||
}
|
}
|
||||||
|
await connect();
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -119,14 +118,13 @@ class ApiProvider {
|
||||||
if (msg.v0.hasResponse()) {
|
if (msg.v0.hasResponse()) {
|
||||||
messagesV0[msg.v0.seq] = msg;
|
messagesV0[msg.v0.seq] = msg;
|
||||||
} else {
|
} else {
|
||||||
print("Got a new message from the server: $msg");
|
log.shout("Got a new message from the server: $msg");
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
log.shout("Error parsing the servers message: $e");
|
log.shout("Error parsing the servers message: $e");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: There must be a smarter move to do that :/
|
|
||||||
Future<server.ServerToClient?> _waitForResponse(Int64 seq) async {
|
Future<server.ServerToClient?> _waitForResponse(Int64 seq) async {
|
||||||
final startTime = DateTime.now();
|
final startTime = DateTime.now();
|
||||||
|
|
||||||
|
|
@ -147,6 +145,9 @@ class ApiProvider {
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<server.ServerToClient?> _sendRequestV0(ClientToServer request) async {
|
Future<server.ServerToClient?> _sendRequestV0(ClientToServer request) async {
|
||||||
|
if (_channel == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
var seq = Int64(Random().nextInt(4294967296));
|
var seq = Int64(Random().nextInt(4294967296));
|
||||||
while (messagesV0.containsKey(seq)) {
|
while (messagesV0.containsKey(seq)) {
|
||||||
seq = Int64(Random().nextInt(4294967296));
|
seq = Int64(Random().nextInt(4294967296));
|
||||||
|
|
@ -155,27 +156,24 @@ class ApiProvider {
|
||||||
|
|
||||||
final requestBytes = request.writeToBuffer();
|
final requestBytes = request.writeToBuffer();
|
||||||
|
|
||||||
log.info("Check if is connected?");
|
|
||||||
// check if it is connected to the backend. if not try to reconnect.
|
|
||||||
if (!await connect(null)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
_channel!.sink.add(requestBytes);
|
_channel!.sink.add(requestBytes);
|
||||||
|
|
||||||
return await _waitForResponse(seq);
|
return await _waitForResponse(seq);
|
||||||
}
|
}
|
||||||
|
|
||||||
ClientToServer createClientToServerFromHandshake(Handshake handshake) {
|
ClientToServer createClientToServerFromHandshake(Handshake handshake) {
|
||||||
// Create the V0 message
|
|
||||||
var v0 = V0()
|
var v0 = V0()
|
||||||
..seq = Int64(0) // You can set this to the appropriate sequence number
|
..seq = Int64(0)
|
||||||
..handshake = handshake;
|
..handshake = handshake;
|
||||||
|
return ClientToServer()..v0 = v0;
|
||||||
|
}
|
||||||
|
|
||||||
// Create the ClientToServer message
|
ClientToServer createClientToServerFromApplicationData(
|
||||||
var clientToServer = ClientToServer()..v0 = v0;
|
ApplicationData applicationData) {
|
||||||
|
var v0 = V0()
|
||||||
return clientToServer;
|
..seq = Int64(0)
|
||||||
|
..applicationdata = applicationData;
|
||||||
|
return ClientToServer()..v0 = v0;
|
||||||
}
|
}
|
||||||
|
|
||||||
static String getLocalizedString(BuildContext context, ErrorCode code) {
|
static String getLocalizedString(BuildContext context, ErrorCode code) {
|
||||||
|
|
@ -219,6 +217,59 @@ class ApiProvider {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future authenticate() async {
|
||||||
|
final reqSignal = await SignalHelper.getRegisterData();
|
||||||
|
|
||||||
|
if (reqSignal == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var handshake = Handshake()..getchallenge = Handshake_GetChallenge();
|
||||||
|
var req = createClientToServerFromHandshake(handshake);
|
||||||
|
|
||||||
|
final resp = await _sendRequestV0(req);
|
||||||
|
if (resp == null) {
|
||||||
|
log.shout("Server is not reachable!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final result = _asResult(resp);
|
||||||
|
if (result.isError) {
|
||||||
|
log.shout(result);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final challenge = result.value.challenge;
|
||||||
|
|
||||||
|
final privKey = await SignalHelper.getPrivateKey();
|
||||||
|
if (privKey == null) return;
|
||||||
|
final random = getRandomUint8List(32);
|
||||||
|
final signature = sign(privKey.serialize(), challenge, random);
|
||||||
|
|
||||||
|
final userData = await getUser();
|
||||||
|
if (userData == null) return;
|
||||||
|
|
||||||
|
var open = Handshake_OpenSession()
|
||||||
|
..response = signature
|
||||||
|
..userId = userData.userId;
|
||||||
|
|
||||||
|
var opensession = Handshake()..opensession = open;
|
||||||
|
|
||||||
|
var req2 = createClientToServerFromHandshake(opensession);
|
||||||
|
|
||||||
|
final resp2 = await _sendRequestV0(req2);
|
||||||
|
if (resp2 == null) {
|
||||||
|
log.shout("Server is not reachable!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final result2 = _asResult(resp2);
|
||||||
|
if (result2.isError) {
|
||||||
|
log.shout(result2);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info("Authenticated!");
|
||||||
|
}
|
||||||
|
|
||||||
Future<Result> register(String username, String? inviteCode) async {
|
Future<Result> register(String username, String? inviteCode) async {
|
||||||
final reqSignal = await SignalHelper.getRegisterData();
|
final reqSignal = await SignalHelper.getRegisterData();
|
||||||
|
|
||||||
|
|
@ -247,4 +298,16 @@ class ApiProvider {
|
||||||
}
|
}
|
||||||
return _asResult(resp);
|
return _asResult(resp);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<Result> getUserData(String username) async {
|
||||||
|
var get = ApplicationData_GetUserByUsername()..username = username;
|
||||||
|
var appData = ApplicationData()..getuserbyusername = get;
|
||||||
|
var req = createClientToServerFromApplicationData(appData);
|
||||||
|
|
||||||
|
final resp = await _sendRequestV0(req);
|
||||||
|
if (resp == null) {
|
||||||
|
return Result.error("Server is not reachable!");
|
||||||
|
}
|
||||||
|
return _asResult(resp);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -139,20 +139,35 @@ class SignalDataModel {
|
||||||
class SignalHelper {
|
class SignalHelper {
|
||||||
static const int defaultDeviceId = 1;
|
static const int defaultDeviceId = 1;
|
||||||
|
|
||||||
static Future<Map<String, dynamic>?> getRegisterData() async {
|
static Future<ECPrivateKey?> getPrivateKey() async {
|
||||||
final storage = getSecureStorage();
|
final storage = getSecureStorage();
|
||||||
final signalIdentityJson = await storage.read(key: "signal_identity");
|
final signalIdentityJson = await storage.read(key: "signal_identity");
|
||||||
if (signalIdentityJson == null) {
|
if (signalIdentityJson == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
print(signalIdentityJson);
|
|
||||||
final SignalIdentity signalIdentity =
|
final SignalIdentity signalIdentity =
|
||||||
SignalIdentity.fromJson(jsonDecode(signalIdentityJson));
|
SignalIdentity.fromJson(jsonDecode(signalIdentityJson));
|
||||||
|
|
||||||
final identityKeyPair =
|
final IdentityKeyPair identityKeyPair =
|
||||||
IdentityKeyPair.fromSerialized(signalIdentity.identityKeyPairU8List);
|
IdentityKeyPair.fromSerialized(signalIdentity.identityKeyPairU8List);
|
||||||
|
|
||||||
|
return identityKeyPair.getPrivateKey();
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<Map<String, dynamic>?> getRegisterData() async {
|
||||||
// final publicKey = identityKeyPair.getPublicKey().serialize();
|
// final publicKey = identityKeyPair.getPublicKey().serialize();
|
||||||
|
final storage = getSecureStorage();
|
||||||
|
final signalIdentityJson = await storage.read(key: "signal_identity");
|
||||||
|
if (signalIdentityJson == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
final SignalIdentity signalIdentity =
|
||||||
|
SignalIdentity.fromJson(jsonDecode(signalIdentityJson));
|
||||||
|
|
||||||
|
final IdentityKeyPair identityKeyPair =
|
||||||
|
IdentityKeyPair.fromSerialized(signalIdentity.identityKeyPairU8List);
|
||||||
|
|
||||||
ConnectSignalProtocolStore signalStore = ConnectSignalProtocolStore(
|
ConnectSignalProtocolStore signalStore = ConnectSignalProtocolStore(
|
||||||
identityKeyPair, signalIdentity.registrationId);
|
identityKeyPair, signalIdentity.registrationId);
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,14 @@
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
import 'dart:math';
|
||||||
|
import 'dart:typed_data';
|
||||||
|
import 'dart:ui';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:logging/logging.dart';
|
||||||
import 'package:twonly/main.dart';
|
import 'package:twonly/main.dart';
|
||||||
import 'package:twonly/src/signal/signal_helper.dart';
|
import 'package:twonly/src/signal/signal_helper.dart';
|
||||||
import 'package:twonly/src/providers/api_provider.dart';
|
import 'package:twonly/src/providers/api_provider.dart';
|
||||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||||
import 'model/user_data_json.dart';
|
import 'model/user_data_json.dart';
|
||||||
import 'package:logging/logging.dart';
|
|
||||||
|
|
||||||
// Just a helper function to get the secure storage
|
// Just a helper function to get the secure storage
|
||||||
FlutterSecureStorage getSecureStorage() {
|
FlutterSecureStorage getSecureStorage() {
|
||||||
|
|
@ -25,17 +29,16 @@ Future<bool> isUserCreated() async {
|
||||||
Future<UserData?> getUser() async {
|
Future<UserData?> getUser() async {
|
||||||
final storage = getSecureStorage();
|
final storage = getSecureStorage();
|
||||||
String? userJson = await storage.read(key: "user_data");
|
String? userJson = await storage.read(key: "user_data");
|
||||||
print(userJson);
|
|
||||||
if (userJson == null) {
|
if (userJson == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
final userMap = jsonDecode(userJson) as Map<String, dynamic>;
|
final userMap = jsonDecode(userJson) as Map<String, dynamic>;
|
||||||
|
Logger("get_user").info("Found user: $userMap");
|
||||||
final user = UserData.fromJson(userMap);
|
final user = UserData.fromJson(userMap);
|
||||||
print(user);
|
|
||||||
return user;
|
return user;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print(e);
|
Logger("get_user").shout("Error getting user: $e");
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -47,16 +50,38 @@ Future<bool> deleteLocalUserData() async {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Uint8List getRandomUint8List(int length) {
|
||||||
|
final Random random = Random.secure();
|
||||||
|
final Uint8List randomBytes = Uint8List(length);
|
||||||
|
|
||||||
|
for (int i = 0; i < length; i++) {
|
||||||
|
randomBytes[i] = random.nextInt(256); // Generate a random byte (0-255)
|
||||||
|
}
|
||||||
|
|
||||||
|
return randomBytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Result> addNewUser(String username) async {
|
||||||
|
final res = await apiProvider.getUserData(username);
|
||||||
|
|
||||||
|
// if (res.isSuccess) {
|
||||||
|
// print("Got user_id ${res.value}");
|
||||||
|
// final userData = UserData(
|
||||||
|
// userId: res.value.userid, username: username, displayName: username);
|
||||||
|
// }
|
||||||
|
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
Future<Result> createNewUser(String username, String inviteCode) async {
|
Future<Result> createNewUser(String username, String inviteCode) async {
|
||||||
final storage = getSecureStorage();
|
final storage = getSecureStorage();
|
||||||
|
|
||||||
await SignalHelper.createIfNotExistsSignalIdentity();
|
await SignalHelper.createIfNotExistsSignalIdentity();
|
||||||
|
|
||||||
// TODO: API call to server to check username and inviteCode
|
|
||||||
final res = await apiProvider.register(username, inviteCode);
|
final res = await apiProvider.register(username, inviteCode);
|
||||||
|
|
||||||
if (res.isSuccess) {
|
if (res.isSuccess) {
|
||||||
print("Got user_id ${res.value}");
|
Logger("create_new_user").info("Got user_id ${res.value} from server");
|
||||||
final userData = UserData(
|
final userData = UserData(
|
||||||
userId: res.value.userid, username: username, displayName: username);
|
userId: res.value.userid, username: username, displayName: username);
|
||||||
storage.write(key: "user_data", value: jsonEncode(userData));
|
storage.write(key: "user_data", value: jsonEncode(userData));
|
||||||
|
|
@ -64,3 +89,88 @@ Future<Result> createNewUser(String username, String inviteCode) async {
|
||||||
|
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget createInitialsAvatar(String username, bool isDarkMode) {
|
||||||
|
// Extract initials from the username
|
||||||
|
List<String> nameParts = username.split(' ');
|
||||||
|
String initials = nameParts.map((part) => part[0]).join().toUpperCase();
|
||||||
|
if (initials.length > 2) {
|
||||||
|
initials = initials[0] + initials[1];
|
||||||
|
} else if (initials.length == 1) {
|
||||||
|
initials = username[0] + username[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
initials = initials.toUpperCase();
|
||||||
|
|
||||||
|
// Generate a color based on the initials (you can customize this logic)
|
||||||
|
Color avatarColor = _getColorFromUsername(username, isDarkMode);
|
||||||
|
|
||||||
|
return CircleAvatar(
|
||||||
|
backgroundColor: avatarColor,
|
||||||
|
child: Text(
|
||||||
|
initials,
|
||||||
|
style: TextStyle(
|
||||||
|
color: _getTextColor(avatarColor),
|
||||||
|
fontWeight: FontWeight.normal,
|
||||||
|
fontSize: 20),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Color _getTextColor(Color color) {
|
||||||
|
double value = 100.0;
|
||||||
|
// Ensure the value does not exceed the RGB limits
|
||||||
|
int newRed = ((color.r * 255) - value).clamp(0, 255).round();
|
||||||
|
int newGreen = (color.g * 255 - value).clamp(0, 255).round();
|
||||||
|
int newBlue = (color.b * 255 - value).clamp(0, 255).round();
|
||||||
|
|
||||||
|
return Color.fromARGB((color.a * 255).round(), newRed, newGreen, newBlue);
|
||||||
|
}
|
||||||
|
|
||||||
|
Color _getColorFromUsername(String username, bool isDarkMode) {
|
||||||
|
// Define color lists for light and dark themes
|
||||||
|
List<Color> lightColors = [
|
||||||
|
Colors.red,
|
||||||
|
Colors.green,
|
||||||
|
Colors.blue,
|
||||||
|
Colors.orange,
|
||||||
|
Colors.purple,
|
||||||
|
Colors.teal,
|
||||||
|
Colors.amber,
|
||||||
|
Colors.indigo,
|
||||||
|
Colors.cyan,
|
||||||
|
Colors.lime,
|
||||||
|
Colors.pink,
|
||||||
|
Colors.brown,
|
||||||
|
Colors.grey,
|
||||||
|
];
|
||||||
|
|
||||||
|
List<Color> darkColors = [
|
||||||
|
const Color.fromARGB(255, 246, 227, 254), // Light Lavender
|
||||||
|
const Color.fromARGB(255, 246, 216, 215), // Light Pink
|
||||||
|
const Color.fromARGB(255, 226, 236, 235), // Light Teal
|
||||||
|
const Color.fromARGB(255, 255, 224, 178), // Light Yellow
|
||||||
|
const Color.fromARGB(255, 255, 182, 193), // Light Pink (Hot Pink)
|
||||||
|
const Color.fromARGB(255, 173, 216, 230), // Light Blue
|
||||||
|
const Color.fromARGB(255, 221, 160, 221), // Plum
|
||||||
|
const Color.fromARGB(255, 255, 228, 196), // Bisque
|
||||||
|
const Color.fromARGB(255, 240, 230, 140), // Khaki
|
||||||
|
const Color.fromARGB(255, 255, 192, 203), // Pink
|
||||||
|
const Color.fromARGB(255, 255, 218, 185), // Peach Puff
|
||||||
|
const Color.fromARGB(255, 255, 160, 122), // Light Salmon
|
||||||
|
const Color.fromARGB(255, 135, 206, 250), // Light Sky Blue
|
||||||
|
const Color.fromARGB(255, 255, 228, 225), // Misty Rose
|
||||||
|
const Color.fromARGB(255, 240, 248, 255), // Alice Blue
|
||||||
|
const Color.fromARGB(255, 255, 250, 205), // Lemon Chiffon
|
||||||
|
const Color.fromARGB(255, 255, 218, 185), // Peach Puff
|
||||||
|
];
|
||||||
|
|
||||||
|
// Simple logic to generate a hash from initials
|
||||||
|
int hash = username.codeUnits.fold(0, (prev, element) => prev + element);
|
||||||
|
|
||||||
|
// Select the appropriate color list based on the current theme brightness
|
||||||
|
List<Color> colors = isDarkMode ? darkColors : lightColors;
|
||||||
|
|
||||||
|
// Use the hash to select a color from the list
|
||||||
|
return colors[hash % colors.length];
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -33,9 +33,9 @@ class CameraPage extends StatelessWidget {
|
||||||
previewAlignment: Alignment.topRight,
|
previewAlignment: Alignment.topRight,
|
||||||
// Buttons of CamerAwesome UI will use this theme
|
// Buttons of CamerAwesome UI will use this theme
|
||||||
theme: AwesomeTheme(
|
theme: AwesomeTheme(
|
||||||
bottomActionsBackgroundColor: Colors.cyan.withOpacity(0.5),
|
bottomActionsBackgroundColor: Colors.cyan.withAlpha(150),
|
||||||
buttonTheme: AwesomeButtonTheme(
|
buttonTheme: AwesomeButtonTheme(
|
||||||
backgroundColor: Colors.cyan.withOpacity(0.5),
|
backgroundColor: Colors.cyan.withAlpha(150),
|
||||||
iconSize: 20,
|
iconSize: 20,
|
||||||
foregroundColor: Colors.white,
|
foregroundColor: Colors.white,
|
||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.all(16),
|
||||||
|
|
@ -47,7 +47,7 @@ class CameraPage extends StatelessWidget {
|
||||||
shape: const CircleBorder(),
|
shape: const CircleBorder(),
|
||||||
child: InkWell(
|
child: InkWell(
|
||||||
splashColor: Colors.cyan,
|
splashColor: Colors.cyan,
|
||||||
highlightColor: Colors.cyan.withOpacity(0.5),
|
highlightColor: Colors.cyan.withAlpha(150),
|
||||||
onTap: onTap,
|
onTap: onTap,
|
||||||
child: child,
|
child: child,
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -1,148 +0,0 @@
|
||||||
import 'dart:async';
|
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:mobile_scanner/mobile_scanner.dart';
|
|
||||||
|
|
||||||
class AddNewUserView extends StatefulWidget {
|
|
||||||
const AddNewUserView({super.key});
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<AddNewUserView> createState() => _AddNewUserViewState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _AddNewUserViewState extends State<AddNewUserView>
|
|
||||||
with WidgetsBindingObserver {
|
|
||||||
Barcode? _barcode;
|
|
||||||
final MobileScannerController controller = MobileScannerController(
|
|
||||||
// required options for the scanner
|
|
||||||
);
|
|
||||||
void _handleBarcode(BarcodeCapture barcodes) {
|
|
||||||
if (mounted) {
|
|
||||||
setState(() {
|
|
||||||
_barcode = barcodes.barcodes.firstOrNull;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void didChangeAppLifecycleState(AppLifecycleState state) {
|
|
||||||
// If the controller is not ready, do not try to start or stop it.
|
|
||||||
// Permission dialogs can trigger lifecycle changes before the controller is ready.
|
|
||||||
if (!controller.value.hasCameraPermission) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (state) {
|
|
||||||
case AppLifecycleState.detached:
|
|
||||||
case AppLifecycleState.hidden:
|
|
||||||
case AppLifecycleState.paused:
|
|
||||||
return;
|
|
||||||
case AppLifecycleState.resumed:
|
|
||||||
// Restart the scanner when the app is resumed.
|
|
||||||
// Don't forget to resume listening to the barcode events.
|
|
||||||
_subscription = controller.barcodes.listen(_handleBarcode);
|
|
||||||
|
|
||||||
unawaited(controller.start());
|
|
||||||
case AppLifecycleState.inactive:
|
|
||||||
// Stop the scanner when the app is paused.
|
|
||||||
// Also stop the barcode events subscription.
|
|
||||||
unawaited(_subscription?.cancel());
|
|
||||||
_subscription = null;
|
|
||||||
unawaited(controller.stop());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
// Start listening to lifecycle changes.
|
|
||||||
WidgetsBinding.instance.addObserver(this);
|
|
||||||
|
|
||||||
// Start listening to the barcode events.
|
|
||||||
_subscription = controller.barcodes.listen(_handleBarcode);
|
|
||||||
|
|
||||||
// Finally, start the scanner itself.
|
|
||||||
unawaited(controller.start());
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<void> dispose() async {
|
|
||||||
// Stop listening to lifecycle changes.
|
|
||||||
WidgetsBinding.instance.removeObserver(this);
|
|
||||||
// Stop listening to the barcode events.
|
|
||||||
unawaited(_subscription?.cancel());
|
|
||||||
_subscription = null;
|
|
||||||
// Dispose the widget itself.
|
|
||||||
super.dispose();
|
|
||||||
// Finally, dispose of the controller.
|
|
||||||
await controller.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildBarcode(Barcode? value) {
|
|
||||||
if (value == null) {
|
|
||||||
return Text.rich(
|
|
||||||
TextSpan(
|
|
||||||
children: [
|
|
||||||
TextSpan(
|
|
||||||
text:
|
|
||||||
"One of Connect's most important goals is data confidentiality. The Signal Protocol is therefore used to encrypt the data. The security of the Signal Protocol is based on the fact that the Signal identity is verified in a secure way (you stand next to the person and compare the public key). ",
|
|
||||||
style: TextStyle(color: Colors.white),
|
|
||||||
),
|
|
||||||
TextSpan(
|
|
||||||
text:
|
|
||||||
"To enforce this, the only way to add a new user is to scan their QR code.",
|
|
||||||
style: TextStyle(
|
|
||||||
color: Colors.white,
|
|
||||||
fontWeight: FontWeight.bold), // Bold style
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
overflow: TextOverflow.fade,
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Text(
|
|
||||||
value.displayValue ?? 'No display value.',
|
|
||||||
overflow: TextOverflow.fade,
|
|
||||||
style: const TextStyle(color: Colors.white),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
StreamSubscription<Object?>? _subscription;
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Scaffold(
|
|
||||||
appBar: AppBar(
|
|
||||||
title: Text('Add new user'),
|
|
||||||
),
|
|
||||||
body: Stack(
|
|
||||||
children: [
|
|
||||||
MobileScanner(
|
|
||||||
onDetect: _handleBarcode,
|
|
||||||
),
|
|
||||||
Align(
|
|
||||||
alignment: Alignment.bottomCenter,
|
|
||||||
child: Padding(
|
|
||||||
padding: EdgeInsets.all(20),
|
|
||||||
child: Container(
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Colors.black.withOpacity(0.4),
|
|
||||||
borderRadius: BorderRadius.circular(9),
|
|
||||||
),
|
|
||||||
alignment: Alignment.bottomCenter,
|
|
||||||
height: 200,
|
|
||||||
child: Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
|
||||||
children: [
|
|
||||||
Expanded(child: Center(child: _buildBarcode(_barcode))),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
import 'package:camera/camera.dart';
|
import 'package:camera/camera.dart';
|
||||||
import 'package:flutter/gestures.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
// import 'package:camerawesome/camerawesome_plugin.dart';
|
// import 'package:camerawesome/camerawesome_plugin.dart';
|
||||||
import 'dart:math';
|
import 'dart:math';
|
||||||
|
|
@ -34,14 +33,12 @@ class CameraPreviewViewState extends State<CameraPreviewView> {
|
||||||
// To display the current output from the Camera,
|
// To display the current output from the Camera,
|
||||||
// create a CameraController.
|
// create a CameraController.
|
||||||
|
|
||||||
_controller = CameraController(
|
// _controller = CameraController(
|
||||||
// Get a specific camera from the list of available cameras.
|
// // Get a specific camera from the list of available cameras.
|
||||||
widget.cameras.first,
|
// widget.cameras.first,
|
||||||
// Define the resolution to use.
|
// // Define the resolution to use.
|
||||||
ResolutionPreset.max,
|
// ResolutionPreset.max,
|
||||||
);
|
// );
|
||||||
|
|
||||||
final size = Size(50, 300);
|
|
||||||
|
|
||||||
// _controller.initialize().then((_) {
|
// _controller.initialize().then((_) {
|
||||||
// _controller.value = _controller.value.copyWith(previewSize: size);
|
// _controller.value = _controller.value.copyWith(previewSize: size);
|
||||||
|
|
@ -49,7 +46,7 @@ class CameraPreviewViewState extends State<CameraPreviewView> {
|
||||||
// });
|
// });
|
||||||
|
|
||||||
// Next, initialize the controller. This returns a Future.
|
// Next, initialize the controller. This returns a Future.
|
||||||
_initializeControllerFuture = _controller.initialize();
|
// _initializeControllerFuture = _controller.initialize();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> updateController() async {
|
Future<void> updateController() async {
|
||||||
|
|
@ -59,7 +56,7 @@ class CameraPreviewViewState extends State<CameraPreviewView> {
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
// Dispose of the controller when the widget is disposed.
|
// Dispose of the controller when the widget is disposed.
|
||||||
_controller.dispose();
|
// _controller.dispose();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -67,7 +64,7 @@ class CameraPreviewViewState extends State<CameraPreviewView> {
|
||||||
Future<void> takePicture() async {
|
Future<void> takePicture() async {
|
||||||
try {
|
try {
|
||||||
await _initializeControllerFuture;
|
await _initializeControllerFuture;
|
||||||
final image = await _controller.takePicture();
|
// final image = await _controller.takePicture();
|
||||||
|
|
||||||
if (!context.mounted) return;
|
if (!context.mounted) return;
|
||||||
// await Navigator.of(context).push(
|
// await Navigator.of(context).push(
|
||||||
|
|
@ -129,6 +126,7 @@ class CameraPreviewViewState extends State<CameraPreviewView> {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
return Container();
|
||||||
var isFront = widget.cameras[_selectedCameraIdx].lensDirection ==
|
var isFront = widget.cameras[_selectedCameraIdx].lensDirection ==
|
||||||
CameraLensDirection.front;
|
CameraLensDirection.front;
|
||||||
// Fill this out in the next steps.
|
// Fill this out in the next steps.
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,7 @@
|
||||||
import 'add_new_user_view.dart';
|
import 'package:twonly/src/utils.dart';
|
||||||
|
import 'package:twonly/src/views/search_username_view.dart';
|
||||||
|
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||||
|
import 'new_message_view.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'chat_item_details_view.dart';
|
import 'chat_item_details_view.dart';
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
@ -32,13 +35,13 @@ class ChatListView extends StatefulWidget {
|
||||||
state: MessageSendState.sending),
|
state: MessageSendState.sending),
|
||||||
ChatItem(
|
ChatItem(
|
||||||
userId: 1,
|
userId: 1,
|
||||||
username: "Franz",
|
username: "Klaus",
|
||||||
lastMessageInSeconds: 20829,
|
lastMessageInSeconds: 20829,
|
||||||
flames: 0,
|
flames: 0,
|
||||||
state: MessageSendState.received),
|
state: MessageSendState.received),
|
||||||
ChatItem(
|
ChatItem(
|
||||||
userId: 2,
|
userId: 2,
|
||||||
username: "Heiner",
|
username: "Markus",
|
||||||
lastMessageInSeconds: 291829,
|
lastMessageInSeconds: 291829,
|
||||||
state: MessageSendState.opened,
|
state: MessageSendState.opened,
|
||||||
flames: 38),
|
flames: 38),
|
||||||
|
|
@ -163,85 +166,24 @@ class _ChatListViewState extends State<ChatListView> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget createInitialsAvatar(String username) {
|
|
||||||
// Extract initials from the username
|
|
||||||
List<String> nameParts = username.split(' ');
|
|
||||||
String initials = nameParts.map((part) => part[0]).join().toUpperCase();
|
|
||||||
if (initials.length > 2) {
|
|
||||||
initials = initials[0] + initials[1];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generate a color based on the initials (you can customize this logic)
|
|
||||||
Color avatarColor = _getColorFromInitials(initials);
|
|
||||||
|
|
||||||
return CircleAvatar(
|
|
||||||
backgroundColor: avatarColor,
|
|
||||||
child: Text(
|
|
||||||
initials,
|
|
||||||
style: TextStyle(
|
|
||||||
color: _getTextColor(Colors.white), fontWeight: FontWeight.bold),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Color _getTextColor(Color backgroundColor) {
|
|
||||||
// Calculate the luminance of the background color
|
|
||||||
double luminance = backgroundColor.computeLuminance();
|
|
||||||
// Return white for dark backgrounds and black for light backgrounds
|
|
||||||
return luminance < 0.5 ? Colors.white : Colors.black;
|
|
||||||
}
|
|
||||||
|
|
||||||
Color _getColorFromInitials(String initials) {
|
|
||||||
// Define color lists for light and dark themes
|
|
||||||
List<Color> lightColors = [
|
|
||||||
Colors.red,
|
|
||||||
Colors.green,
|
|
||||||
Colors.blue,
|
|
||||||
Colors.orange,
|
|
||||||
Colors.purple,
|
|
||||||
Colors.teal,
|
|
||||||
Colors.amber,
|
|
||||||
Colors.indigo,
|
|
||||||
Colors.cyan,
|
|
||||||
Colors.lime,
|
|
||||||
Colors.pink,
|
|
||||||
Colors.brown,
|
|
||||||
Colors.grey,
|
|
||||||
];
|
|
||||||
|
|
||||||
List<Color> darkColors = [
|
|
||||||
Colors.deepOrange,
|
|
||||||
Colors.deepPurple,
|
|
||||||
Colors.redAccent,
|
|
||||||
Colors.greenAccent,
|
|
||||||
Colors.blueAccent,
|
|
||||||
Colors.orangeAccent,
|
|
||||||
Colors.purpleAccent,
|
|
||||||
Colors.tealAccent,
|
|
||||||
Colors.amberAccent,
|
|
||||||
Colors.indigoAccent,
|
|
||||||
Colors.cyanAccent,
|
|
||||||
Colors.limeAccent,
|
|
||||||
Colors.pinkAccent,
|
|
||||||
];
|
|
||||||
|
|
||||||
// Simple logic to generate a hash from initials
|
|
||||||
int hash = initials.codeUnits.fold(0, (prev, element) => prev + element);
|
|
||||||
|
|
||||||
// Select the appropriate color list based on the current theme brightness
|
|
||||||
List<Color> colors = Theme.of(context).brightness == Brightness.dark
|
|
||||||
? darkColors
|
|
||||||
: lightColors;
|
|
||||||
|
|
||||||
// Use the hash to select a color from the list
|
|
||||||
return colors[hash % colors.length];
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: const Text('Chats'),
|
title: Text(AppLocalizations.of(context)!.chatsTitle),
|
||||||
|
actions: [
|
||||||
|
IconButton(
|
||||||
|
icon: Icon(Icons.person_add), // User with add icon
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (context) => SearchUsernameView(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
body: ListView.builder(
|
body: ListView.builder(
|
||||||
restorationId: 'sampleItemListView',
|
restorationId: 'sampleItemListView',
|
||||||
|
|
@ -251,7 +193,8 @@ class _ChatListViewState extends State<ChatListView> {
|
||||||
return ListTile(
|
return ListTile(
|
||||||
title: Text(item.username),
|
title: Text(item.username),
|
||||||
subtitle: getSubtitle(item),
|
subtitle: getSubtitle(item),
|
||||||
leading: createInitialsAvatar(item.username),
|
leading: createInitialsAvatar(item.username,
|
||||||
|
Theme.of(context).brightness == Brightness.dark),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
Navigator.push(
|
Navigator.push(
|
||||||
context,
|
context,
|
||||||
|
|
@ -269,11 +212,11 @@ class _ChatListViewState extends State<ChatListView> {
|
||||||
Navigator.push(
|
Navigator.push(
|
||||||
context,
|
context,
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
builder: (context) => AddNewUserView(),
|
builder: (context) => NewMessageView(),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
child: const Icon(Icons.add),
|
child: const Icon(Icons.edit),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
182
lib/src/views/new_message_view.dart
Normal file
182
lib/src/views/new_message_view.dart
Normal file
|
|
@ -0,0 +1,182 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||||
|
import 'package:twonly/src/utils.dart';
|
||||||
|
|
||||||
|
class NewMessageView extends StatefulWidget {
|
||||||
|
const NewMessageView({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<NewMessageView> createState() => _NewMessageView();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _NewMessageView extends State<NewMessageView> {
|
||||||
|
final List<String> _known_users = [
|
||||||
|
"Alisa",
|
||||||
|
"Klaus",
|
||||||
|
"John",
|
||||||
|
"Emma",
|
||||||
|
"Michael",
|
||||||
|
"Sophia",
|
||||||
|
"James",
|
||||||
|
"Olivia",
|
||||||
|
"Liam",
|
||||||
|
"Ava",
|
||||||
|
"Noah",
|
||||||
|
"Isabella",
|
||||||
|
"Lucas",
|
||||||
|
"Mia",
|
||||||
|
"Ethan",
|
||||||
|
"Charlotte",
|
||||||
|
];
|
||||||
|
List<String> _filteredUsers = [];
|
||||||
|
String _lastSearchQuery = '';
|
||||||
|
final TextEditingController searchUserName = TextEditingController();
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
// Initialize the filtered users with all known users
|
||||||
|
_filteredUsers = List.from(_known_users);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future _filterUsers(String query) async {
|
||||||
|
if (query.isEmpty) {
|
||||||
|
_filteredUsers = List.from(_known_users);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (_lastSearchQuery.length < query.length) {
|
||||||
|
_filteredUsers = _known_users
|
||||||
|
.where((user) => user.toLowerCase().contains(query.toLowerCase()))
|
||||||
|
.toList();
|
||||||
|
} else {
|
||||||
|
_filteredUsers = _filteredUsers
|
||||||
|
.where((user) => user.toLowerCase().contains(query.toLowerCase()))
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
InputDecoration getInputDecoration(hintText) {
|
||||||
|
final primaryColor =
|
||||||
|
Theme.of(context).colorScheme.primary; // Get the primary color
|
||||||
|
return InputDecoration(
|
||||||
|
hintText: hintText,
|
||||||
|
focusedBorder: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(9.0),
|
||||||
|
borderSide: BorderSide(color: primaryColor, width: 1.0),
|
||||||
|
),
|
||||||
|
enabledBorder: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(8.0),
|
||||||
|
borderSide: BorderSide(
|
||||||
|
color: Theme.of(context).colorScheme.outline, width: 1.0),
|
||||||
|
),
|
||||||
|
contentPadding: EdgeInsets.symmetric(vertical: 15.0, horizontal: 20.0),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: Text(AppLocalizations.of(context)!.newMessageTitle),
|
||||||
|
),
|
||||||
|
body: Padding(
|
||||||
|
padding: EdgeInsets.only(bottom: 20, left: 10, top: 20, right: 10),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: EdgeInsets.symmetric(horizontal: 10),
|
||||||
|
child: TextField(
|
||||||
|
onChanged: _filterUsers,
|
||||||
|
decoration: getInputDecoration(
|
||||||
|
AppLocalizations.of(context)!.newMessageTitle))),
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
// Step 4: Add buttons at the top
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||||
|
children: [
|
||||||
|
FilledButton(
|
||||||
|
onPressed: () {
|
||||||
|
// Handle Neue Gruppe button press
|
||||||
|
},
|
||||||
|
child: Text('Neue Gruppe'),
|
||||||
|
),
|
||||||
|
FilledButton(
|
||||||
|
onPressed: () {
|
||||||
|
// Handle Nach Nutzername suchen button press
|
||||||
|
},
|
||||||
|
child: Text('Nach Nutzername suchen'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
Expanded(
|
||||||
|
child: UserList(_filteredUsers),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class UserList extends StatelessWidget {
|
||||||
|
final List<String> _known_users;
|
||||||
|
|
||||||
|
UserList(this._known_users);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
// Step 1: Sort the users alphabetically
|
||||||
|
_known_users.sort();
|
||||||
|
|
||||||
|
// Step 2: Group users by their initials
|
||||||
|
Map<String, List<String>> groupedUsers = {};
|
||||||
|
for (var user in _known_users) {
|
||||||
|
String initial = user[0].toUpperCase();
|
||||||
|
if (!groupedUsers.containsKey(initial)) {
|
||||||
|
groupedUsers[initial] = [];
|
||||||
|
}
|
||||||
|
groupedUsers[initial]!.add(user);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 3: Create a list of sections
|
||||||
|
List<MapEntry<String, List<String>>> sections =
|
||||||
|
groupedUsers.entries.toList();
|
||||||
|
|
||||||
|
return ListView.builder(
|
||||||
|
restorationId: 'new_message_users_list',
|
||||||
|
itemCount: sections.length,
|
||||||
|
itemBuilder: (BuildContext context, int sectionIndex) {
|
||||||
|
final section = sections[sectionIndex];
|
||||||
|
final initial = section.key;
|
||||||
|
final users = section.value;
|
||||||
|
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
// Header for the initial
|
||||||
|
Padding(
|
||||||
|
padding:
|
||||||
|
const EdgeInsets.symmetric(vertical: 8.0, horizontal: 16.0),
|
||||||
|
child: Text(
|
||||||
|
initial,
|
||||||
|
style: TextStyle(fontWeight: FontWeight.normal, fontSize: 18),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
// List of users under this initial
|
||||||
|
...users.map((username) {
|
||||||
|
return ListTile(
|
||||||
|
title: Text(username),
|
||||||
|
leading: createInitialsAvatar(
|
||||||
|
username, Theme.of(context).brightness == Brightness.dark),
|
||||||
|
onTap: () {
|
||||||
|
// Handle tap
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}).toList(),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -7,7 +7,7 @@ class PermissionHandlerView extends StatefulWidget {
|
||||||
final Function onSuccess;
|
final Function onSuccess;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
_PermissionHandlerViewState createState() => _PermissionHandlerViewState();
|
PermissionHandlerViewState createState() => PermissionHandlerViewState();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> checkPermissions() async {
|
Future<bool> checkPermissions() async {
|
||||||
|
|
@ -20,7 +20,7 @@ Future<bool> checkPermissions() async {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
class _PermissionHandlerViewState extends State<PermissionHandlerView> {
|
class PermissionHandlerViewState extends State<PermissionHandlerView> {
|
||||||
Future<Map<Permission, PermissionStatus>> permissionServices() async {
|
Future<Map<Permission, PermissionStatus>> permissionServices() async {
|
||||||
// You can request multiple permissions at once.
|
// You can request multiple permissions at once.
|
||||||
Map<Permission, PermissionStatus> statuses = await [
|
Map<Permission, PermissionStatus> statuses = await [
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
import 'package:twonly/src/providers/api_provider.dart';
|
import 'package:twonly/src/providers/api_provider.dart';
|
||||||
|
|
||||||
import '../utils.dart';
|
import '../utils.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
|
|
|
||||||
56
lib/src/views/search_username_view.dart
Normal file
56
lib/src/views/search_username_view.dart
Normal file
|
|
@ -0,0 +1,56 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||||
|
|
||||||
|
class SearchUsernameView extends StatefulWidget {
|
||||||
|
const SearchUsernameView({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<SearchUsernameView> createState() => _SearchUsernameView();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SearchUsernameView extends State<SearchUsernameView> {
|
||||||
|
final TextEditingController searchUserName = TextEditingController();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
InputDecoration getInputDecoration(hintText) {
|
||||||
|
final primaryColor =
|
||||||
|
Theme.of(context).colorScheme.primary; // Get the primary color
|
||||||
|
return InputDecoration(
|
||||||
|
hintText: hintText,
|
||||||
|
focusedBorder: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(9.0),
|
||||||
|
borderSide: BorderSide(color: primaryColor, width: 1.0),
|
||||||
|
),
|
||||||
|
enabledBorder: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(8.0),
|
||||||
|
borderSide: BorderSide(
|
||||||
|
color: Theme.of(context).colorScheme.outline, width: 1.0),
|
||||||
|
),
|
||||||
|
contentPadding: EdgeInsets.symmetric(vertical: 15.0, horizontal: 20.0),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: Text(AppLocalizations.of(context)!.searchUsernameTitle),
|
||||||
|
),
|
||||||
|
body: Padding(
|
||||||
|
padding: EdgeInsets.only(bottom: 20, left: 10, top: 20, right: 10),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: EdgeInsets.symmetric(horizontal: 10),
|
||||||
|
child: TextField(
|
||||||
|
onSubmitted: (value) {
|
||||||
|
print(value);
|
||||||
|
},
|
||||||
|
decoration: getInputDecoration(
|
||||||
|
AppLocalizations.of(context)!.searchUsernameInput))),
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
10
pubspec.lock
10
pubspec.lock
|
|
@ -634,14 +634,6 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.0"
|
version: "2.0.0"
|
||||||
mobile_scanner:
|
|
||||||
dependency: "direct main"
|
|
||||||
description:
|
|
||||||
name: mobile_scanner
|
|
||||||
sha256: "57d6269d10912d5d583606b46d963d7c5d0299d2c37add8b7192dd769d40a319"
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "6.0.3"
|
|
||||||
nested:
|
nested:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
@ -827,7 +819,7 @@ packages:
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "7.6.4"
|
version: "7.6.4"
|
||||||
protobuf:
|
protobuf:
|
||||||
dependency: transitive
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: protobuf
|
name: protobuf
|
||||||
sha256: "01dd9bd0fa02548bf2ceee13545d4a0ec6046459d847b6b061d8a27237108a08"
|
sha256: "01dd9bd0fa02548bf2ceee13545d4a0ec6046459d847b6b061d8a27237108a08"
|
||||||
|
|
|
||||||
|
|
@ -26,11 +26,11 @@ dependencies:
|
||||||
json_annotation: ^4.9.0
|
json_annotation: ^4.9.0
|
||||||
libsignal_protocol_dart: ^0.7.1
|
libsignal_protocol_dart: ^0.7.1
|
||||||
logging: ^1.3.0
|
logging: ^1.3.0
|
||||||
mobile_scanner: ^6.0.2
|
|
||||||
path: ^1.9.0
|
path: ^1.9.0
|
||||||
path_provider: ^2.1.5
|
path_provider: ^2.1.5
|
||||||
permission_handler: ^11.3.1
|
permission_handler: ^11.3.1
|
||||||
pro_image_editor: ^7.6.4
|
pro_image_editor: ^7.6.4
|
||||||
|
protobuf: ^2.1.0
|
||||||
provider: ^6.1.2
|
provider: ^6.1.2
|
||||||
restart_app: ^1.3.2
|
restart_app: ^1.3.2
|
||||||
sqflite_sqlcipher: ^3.1.0+1
|
sqflite_sqlcipher: ^3.1.0+1
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue