From 20c20eb1e13d5956fd247714f972fffeafa8ecd8 Mon Sep 17 00:00:00 2001 From: otsmr Date: Sat, 25 Jan 2025 03:30:41 +0100 Subject: [PATCH] follow request --- lib/main.dart | 8 +- lib/src/localization/app_en.arb | 2 +- lib/src/model/contacts_model.dart | 74 ++++++++++-- lib/src/model/json/message.dart | 25 ++-- lib/src/model/json/message.g.dart | 2 - lib/src/model/model_constants.dart | 2 +- lib/src/proto/api/client_to_server.pb.dart | 70 ++++++++++- .../proto/api/client_to_server.pbjson.dart | 37 ++++-- lib/src/proto/api/server_to_client.pb.dart | 28 +++++ .../proto/api/server_to_client.pbjson.dart | 37 +++--- lib/src/providers/api_provider.dart | 61 ++++++++-- lib/src/providers/db_provider.dart | 2 +- lib/src/signal/connect_pre_key_store.dart | 3 +- lib/src/signal/connect_session_store.dart | 2 +- lib/src/signal/signal_helper.dart | 86 ------------- lib/src/utils/api.dart | 30 ++++- lib/src/utils/signal.dart | 113 +++++++++++++++++- lib/src/views/chat_list_view.dart | 52 ++++++-- lib/src/views/search_username_view.dart | 96 ++++++++++++--- 19 files changed, 533 insertions(+), 197 deletions(-) delete mode 100644 lib/src/signal/signal_helper.dart diff --git a/lib/main.dart b/lib/main.dart index 8934722..159b535 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -36,10 +36,10 @@ void main() async { var apiUrl = "ws://api.twonly.eu/api/client"; var backupApiUrl = "ws://api2.twonly.eu/api/client"; - if (!kReleaseMode) { - // Overwrite the domain in your local network so you can test the app locally - apiUrl = "ws://10.99.0.6:3030/api/client"; - } + // if (!kReleaseMode) { + // Overwrite the domain in your local network so you can test the app locally + apiUrl = "ws://10.99.0.6:3030/api/client"; + // } apiProvider = ApiProvider(apiUrl: apiUrl, backupApiUrl: backupApiUrl); diff --git a/lib/src/localization/app_en.arb b/lib/src/localization/app_en.arb index d2bf192..aa49b6f 100644 --- a/lib/src/localization/app_en.arb +++ b/lib/src/localization/app_en.arb @@ -20,7 +20,7 @@ "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.", "errorTooManyRequests": "You have made too many requests in a short period. Please wait a moment before trying again.", - "errorInternalError": "The server encountered an internal error. Please try again later.", + "errorInternalError": "The server is currently not available. Please try again later.", "errorInvalidInvitationCode": "The invitation code you provided is invalid. Please check the code and try again.", "errorUsernameAlreadyTaken": "The username you want to use is already taken. Please choose a different username.", "errorSignatureNotValid": "The provided signature is not valid. Please check your credentials and try again.", diff --git a/lib/src/model/contacts_model.dart b/lib/src/model/contacts_model.dart index ffe0c21..e53c326 100644 --- a/lib/src/model/contacts_model.dart +++ b/lib/src/model/contacts_model.dart @@ -1,11 +1,18 @@ import 'package:cv/cv.dart'; import 'package:fixnum/fixnum.dart'; +import 'package:logging/logging.dart'; import 'package:twonly/main.dart'; class Contact { - Contact({required this.userId, required this.displayName}); + Contact( + {required this.userId, + required this.displayName, + required this.accepted, + required this.requested}); final Int64 userId; final String displayName; + final bool accepted; + final bool requested; } class DbContacts extends CvModelBase { @@ -17,6 +24,12 @@ class DbContacts extends CvModelBase { static const columnDisplayName = "display_name"; final displayName = CvField(columnDisplayName); + static const columnAccepted = "accepted"; + final accepted = CvField(columnAccepted); + + static const columnRequested = "requested"; + final requested = CvField(columnRequested); + static const columnCreatedAt = "created_at"; final createdAt = CvField(columnCreatedAt); @@ -25,25 +38,60 @@ class DbContacts extends CvModelBase { CREATE TABLE $tableName ( $columnUserId INTEGER NOT NULL PRIMARY KEY, $columnDisplayName TEXT, + $columnAccepted INT NOT NULL DEFAULT 0, + $columnRequested INT NOT NULL DEFAULT 0, $columnCreatedAt DATETIME DEFAULT CURRENT_TIMESTAMP ) """; } - static Future> getUsers() async { - var users = await dbProvider.db!.query(tableName, - columns: [columnUserId, columnDisplayName, columnCreatedAt]); - if (users.isEmpty) return []; + @override + List get fields => + [userId, displayName, accepted, requested, createdAt]; - List parsedUsers = []; - for (int i = 0; i < users.length; i++) { - parsedUsers.add(Contact( - userId: Int64(users.cast()[i][columnUserId]), - displayName: users.cast()[i][columnDisplayName])); + static Future> getUsers() async { + try { + var users = await dbProvider.db!.query(tableName, columns: [ + columnUserId, + columnDisplayName, + columnAccepted, + columnRequested, + columnCreatedAt + ]); + if (users.isEmpty) return []; + + List parsedUsers = []; + for (int i = 0; i < users.length; i++) { + print(users[i]); + parsedUsers.add( + Contact( + userId: Int64(users.cast()[i][columnUserId]), + displayName: users.cast()[i][columnDisplayName], + accepted: users[i][columnAccepted] == 1, + requested: users[i][columnRequested] == 1, + ), + ); + } + return parsedUsers; + } catch (e) { + Logger("contacts_model/getUsers").shout("$e"); + return []; } - return parsedUsers; } - @override - List get fields => [userId, createdAt]; + static Future insertNewContact( + String username, int userId, bool requested) async { + try { + int a = requested ? 1 : 0; + await dbProvider.db!.insert(DbContacts.tableName, { + DbContacts.columnDisplayName: username, + DbContacts.columnUserId: userId, + DbContacts.columnRequested: a + }); + return true; + } catch (e) { + Logger("contacts_model/getUsers").shout("$e"); + return false; + } + } } diff --git a/lib/src/model/json/message.dart b/lib/src/model/json/message.dart index 96f05e3..77736c6 100644 --- a/lib/src/model/json/message.dart +++ b/lib/src/model/json/message.dart @@ -1,5 +1,5 @@ +import 'dart:convert'; import 'package:json_annotation/json_annotation.dart'; -import 'package:fixnum/fixnum.dart'; import 'package:twonly/src/utils/json.dart'; part 'message.g.dart'; @@ -14,23 +14,19 @@ class _MessageKind { @JsonSerializable() class Message { @Int64Converter() - final Int64 fromUserId; final MessageKind kind; final MessageContent? content; DateTime timestamp; - Message( - {required this.fromUserId, - required this.kind, - this.content, - required this.timestamp}); + Message({required this.kind, this.content, required this.timestamp}); @override String toString() { return 'Message(kind: $kind, content: $content, timestamp: $timestamp)'; } - Message fromJson(Map json) { + static Message fromJson(String jsonString) { + Map json = jsonDecode(jsonString); dynamic content; MessageKind kind = $enumDecode(_$MessageKindEnumMap, json['kind']); switch (kind) { @@ -44,22 +40,19 @@ class Message { } return Message( - fromUserId: const Int64Converter().fromJson(json['fromUserId'] as String), kind: kind, timestamp: DateTime.parse(json['timestamp'] as String), content: content, ); } - Map toJson(Message instance) { + String toJson() { var json = { - 'fromUserId': const Int64Converter().toJson(instance.fromUserId), - 'kind': _$MessageKindEnumMap[instance.kind]!, - 'timestamp': instance.timestamp.toIso8601String(), - 'content': instance.content + 'kind': _$MessageKindEnumMap[kind]!, + 'timestamp': timestamp.toIso8601String(), + 'content': content }; - - return json; + return jsonEncode(json); } } diff --git a/lib/src/model/json/message.g.dart b/lib/src/model/json/message.g.dart index 2dc4a13..5c8b056 100644 --- a/lib/src/model/json/message.g.dart +++ b/lib/src/model/json/message.g.dart @@ -22,7 +22,6 @@ const _$MessageKindEnumMap = { }; Message _$MessageFromJson(Map json) => Message( - fromUserId: const Int64Converter().fromJson(json['fromUserId'] as String), kind: $enumDecode(_$MessageKindEnumMap, json['kind']), content: json['content'] == null ? null @@ -31,7 +30,6 @@ Message _$MessageFromJson(Map json) => Message( ); Map _$MessageToJson(Message instance) => { - 'fromUserId': const Int64Converter().toJson(instance.fromUserId), 'kind': _$MessageKindEnumMap[instance.kind]!, 'content': instance.content, 'timestamp': instance.timestamp.toIso8601String(), diff --git a/lib/src/model/model_constants.dart b/lib/src/model/model_constants.dart index 083e57c..ab1dca4 100644 --- a/lib/src/model/model_constants.dart +++ b/lib/src/model/model_constants.dart @@ -1,5 +1,5 @@ const String dbName = 'twonly.db'; -const int kVersion1 = 1; +const int kVersion1 = 3; String tableLibSignal = 'LibSignal'; diff --git a/lib/src/proto/api/client_to_server.pb.dart b/lib/src/proto/api/client_to_server.pb.dart index b10bed4..851984e 100644 --- a/lib/src/proto/api/client_to_server.pb.dart +++ b/lib/src/proto/api/client_to_server.pb.dart @@ -641,6 +641,56 @@ class ApplicationData_GetUserByUsername extends $pb.GeneratedMessage { void clearUsername() => clearField(1); } +class ApplicationData_GetUserById extends $pb.GeneratedMessage { + factory ApplicationData_GetUserById({ + $fixnum.Int64? userId, + }) { + final $result = create(); + if (userId != null) { + $result.userId = userId; + } + return $result; + } + ApplicationData_GetUserById._() : super(); + factory ApplicationData_GetUserById.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory ApplicationData_GetUserById.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'ApplicationData.GetUserById', package: const $pb.PackageName(_omitMessageNames ? '' : 'client_to_server'), createEmptyInstance: create) + ..aInt64(1, _omitFieldNames ? '' : 'userId') + ..hasRequiredFields = false + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + ApplicationData_GetUserById clone() => ApplicationData_GetUserById()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + ApplicationData_GetUserById copyWith(void Function(ApplicationData_GetUserById) updates) => super.copyWith((message) => updates(message as ApplicationData_GetUserById)) as ApplicationData_GetUserById; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static ApplicationData_GetUserById create() => ApplicationData_GetUserById._(); + ApplicationData_GetUserById createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static ApplicationData_GetUserById getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static ApplicationData_GetUserById? _defaultInstance; + + @$pb.TagNumber(1) + $fixnum.Int64 get userId => $_getI64(0); + @$pb.TagNumber(1) + set userId($fixnum.Int64 v) { $_setInt64(0, v); } + @$pb.TagNumber(1) + $core.bool hasUserId() => $_has(0); + @$pb.TagNumber(1) + void clearUserId() => clearField(1); +} + class ApplicationData_GetPrekeysByUserId extends $pb.GeneratedMessage { factory ApplicationData_GetPrekeysByUserId({ $fixnum.Int64? userId, @@ -825,6 +875,7 @@ enum ApplicationData_ApplicationData { getprekeysbyuserid, getuploadtoken, uploaddata, + getuserbyid, notSet } @@ -835,6 +886,7 @@ class ApplicationData extends $pb.GeneratedMessage { ApplicationData_GetPrekeysByUserId? getprekeysbyuserid, ApplicationData_GetUploadToken? getuploadtoken, ApplicationData_UploadData? uploaddata, + ApplicationData_GetUserById? getuserbyid, }) { final $result = create(); if (textmessage != null) { @@ -852,6 +904,9 @@ class ApplicationData extends $pb.GeneratedMessage { if (uploaddata != null) { $result.uploaddata = uploaddata; } + if (getuserbyid != null) { + $result.getuserbyid = getuserbyid; + } return $result; } ApplicationData._() : super(); @@ -864,15 +919,17 @@ class ApplicationData extends $pb.GeneratedMessage { 3 : ApplicationData_ApplicationData.getprekeysbyuserid, 4 : ApplicationData_ApplicationData.getuploadtoken, 5 : ApplicationData_ApplicationData.uploaddata, + 6 : ApplicationData_ApplicationData.getuserbyid, 0 : ApplicationData_ApplicationData.notSet }; static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'ApplicationData', package: const $pb.PackageName(_omitMessageNames ? '' : 'client_to_server'), createEmptyInstance: create) - ..oo(0, [1, 2, 3, 4, 5]) + ..oo(0, [1, 2, 3, 4, 5, 6]) ..aOM(1, _omitFieldNames ? '' : 'textmessage', subBuilder: ApplicationData_TextMessage.create) ..aOM(2, _omitFieldNames ? '' : 'getuserbyusername', subBuilder: ApplicationData_GetUserByUsername.create) ..aOM(3, _omitFieldNames ? '' : 'getprekeysbyuserid', subBuilder: ApplicationData_GetPrekeysByUserId.create) ..aOM(4, _omitFieldNames ? '' : 'getuploadtoken', subBuilder: ApplicationData_GetUploadToken.create) ..aOM(5, _omitFieldNames ? '' : 'uploaddata', subBuilder: ApplicationData_UploadData.create) + ..aOM(6, _omitFieldNames ? '' : 'getuserbyid', subBuilder: ApplicationData_GetUserById.create) ..hasRequiredFields = false ; @@ -954,6 +1011,17 @@ class ApplicationData extends $pb.GeneratedMessage { void clearUploaddata() => clearField(5); @$pb.TagNumber(5) ApplicationData_UploadData ensureUploaddata() => $_ensure(4); + + @$pb.TagNumber(6) + ApplicationData_GetUserById get getuserbyid => $_getN(5); + @$pb.TagNumber(6) + set getuserbyid(ApplicationData_GetUserById v) { setField(6, v); } + @$pb.TagNumber(6) + $core.bool hasGetuserbyid() => $_has(5); + @$pb.TagNumber(6) + void clearGetuserbyid() => clearField(6); + @$pb.TagNumber(6) + ApplicationData_GetUserById ensureGetuserbyid() => $_ensure(5); } class Response_PreKey extends $pb.GeneratedMessage { diff --git a/lib/src/proto/api/client_to_server.pbjson.dart b/lib/src/proto/api/client_to_server.pbjson.dart index ef6e4a7..3c52508 100644 --- a/lib/src/proto/api/client_to_server.pbjson.dart +++ b/lib/src/proto/api/client_to_server.pbjson.dart @@ -117,11 +117,12 @@ const ApplicationData$json = { '2': [ {'1': 'textmessage', '3': 1, '4': 1, '5': 11, '6': '.client_to_server.ApplicationData.TextMessage', '9': 0, '10': 'textmessage'}, {'1': 'getuserbyusername', '3': 2, '4': 1, '5': 11, '6': '.client_to_server.ApplicationData.GetUserByUsername', '9': 0, '10': 'getuserbyusername'}, + {'1': 'getuserbyid', '3': 6, '4': 1, '5': 11, '6': '.client_to_server.ApplicationData.GetUserById', '9': 0, '10': 'getuserbyid'}, {'1': 'getprekeysbyuserid', '3': 3, '4': 1, '5': 11, '6': '.client_to_server.ApplicationData.GetPrekeysByUserId', '9': 0, '10': 'getprekeysbyuserid'}, {'1': 'getuploadtoken', '3': 4, '4': 1, '5': 11, '6': '.client_to_server.ApplicationData.GetUploadToken', '9': 0, '10': 'getuploadtoken'}, {'1': 'uploaddata', '3': 5, '4': 1, '5': 11, '6': '.client_to_server.ApplicationData.UploadData', '9': 0, '10': 'uploaddata'}, ], - '3': [ApplicationData_TextMessage$json, ApplicationData_GetUserByUsername$json, ApplicationData_GetPrekeysByUserId$json, ApplicationData_GetUploadToken$json, ApplicationData_UploadData$json], + '3': [ApplicationData_TextMessage$json, ApplicationData_GetUserByUsername$json, ApplicationData_GetUserById$json, ApplicationData_GetPrekeysByUserId$json, ApplicationData_GetUploadToken$json, ApplicationData_UploadData$json], '8': [ {'1': 'ApplicationData'}, ], @@ -144,6 +145,14 @@ const ApplicationData_GetUserByUsername$json = { ], }; +@$core.Deprecated('Use applicationDataDescriptor instead') +const ApplicationData_GetUserById$json = { + '1': 'GetUserById', + '2': [ + {'1': 'user_id', '3': 1, '4': 1, '5': 3, '10': 'userId'}, + ], +}; + @$core.Deprecated('Use applicationDataDescriptor instead') const ApplicationData_GetPrekeysByUserId$json = { '1': 'GetPrekeysByUserId', @@ -175,18 +184,20 @@ final $typed_data.Uint8List applicationDataDescriptor = $convert.base64Decode( 'Cg9BcHBsaWNhdGlvbkRhdGESUQoLdGV4dG1lc3NhZ2UYASABKAsyLS5jbGllbnRfdG9fc2Vydm' 'VyLkFwcGxpY2F0aW9uRGF0YS5UZXh0TWVzc2FnZUgAUgt0ZXh0bWVzc2FnZRJjChFnZXR1c2Vy' 'Ynl1c2VybmFtZRgCIAEoCzIzLmNsaWVudF90b19zZXJ2ZXIuQXBwbGljYXRpb25EYXRhLkdldF' - 'VzZXJCeVVzZXJuYW1lSABSEWdldHVzZXJieXVzZXJuYW1lEmYKEmdldHByZWtleXNieXVzZXJp' - 'ZBgDIAEoCzI0LmNsaWVudF90b19zZXJ2ZXIuQXBwbGljYXRpb25EYXRhLkdldFByZWtleXNCeV' - 'VzZXJJZEgAUhJnZXRwcmVrZXlzYnl1c2VyaWQSWgoOZ2V0dXBsb2FkdG9rZW4YBCABKAsyMC5j' - 'bGllbnRfdG9fc2VydmVyLkFwcGxpY2F0aW9uRGF0YS5HZXRVcGxvYWRUb2tlbkgAUg5nZXR1cG' - 'xvYWR0b2tlbhJOCgp1cGxvYWRkYXRhGAUgASgLMiwuY2xpZW50X3RvX3NlcnZlci5BcHBsaWNh' - 'dGlvbkRhdGEuVXBsb2FkRGF0YUgAUgp1cGxvYWRkYXRhGjoKC1RleHRNZXNzYWdlEhcKB3VzZX' - 'JfaWQYASABKANSBnVzZXJJZBISCgRib2R5GAMgASgMUgRib2R5Gi8KEUdldFVzZXJCeVVzZXJu' - 'YW1lEhoKCHVzZXJuYW1lGAEgASgJUgh1c2VybmFtZRotChJHZXRQcmVrZXlzQnlVc2VySWQSFw' - 'oHdXNlcl9pZBgBIAEoA1IGdXNlcklkGiIKDkdldFVwbG9hZFRva2VuEhAKA2xlbhgBIAEoDVID' - 'bGVuGlsKClVwbG9hZERhdGESIQoMdXBsb2FkX3Rva2VuGAEgASgMUgt1cGxvYWRUb2tlbhIWCg' - 'ZvZmZzZXQYAiABKA1SBm9mZnNldBISCgRkYXRhGAMgASgMUgRkYXRhQhEKD0FwcGxpY2F0aW9u' - 'RGF0YQ=='); + 'VzZXJCeVVzZXJuYW1lSABSEWdldHVzZXJieXVzZXJuYW1lElEKC2dldHVzZXJieWlkGAYgASgL' + 'Mi0uY2xpZW50X3RvX3NlcnZlci5BcHBsaWNhdGlvbkRhdGEuR2V0VXNlckJ5SWRIAFILZ2V0dX' + 'NlcmJ5aWQSZgoSZ2V0cHJla2V5c2J5dXNlcmlkGAMgASgLMjQuY2xpZW50X3RvX3NlcnZlci5B' + 'cHBsaWNhdGlvbkRhdGEuR2V0UHJla2V5c0J5VXNlcklkSABSEmdldHByZWtleXNieXVzZXJpZB' + 'JaCg5nZXR1cGxvYWR0b2tlbhgEIAEoCzIwLmNsaWVudF90b19zZXJ2ZXIuQXBwbGljYXRpb25E' + 'YXRhLkdldFVwbG9hZFRva2VuSABSDmdldHVwbG9hZHRva2VuEk4KCnVwbG9hZGRhdGEYBSABKA' + 'syLC5jbGllbnRfdG9fc2VydmVyLkFwcGxpY2F0aW9uRGF0YS5VcGxvYWREYXRhSABSCnVwbG9h' + 'ZGRhdGEaOgoLVGV4dE1lc3NhZ2USFwoHdXNlcl9pZBgBIAEoA1IGdXNlcklkEhIKBGJvZHkYAy' + 'ABKAxSBGJvZHkaLwoRR2V0VXNlckJ5VXNlcm5hbWUSGgoIdXNlcm5hbWUYASABKAlSCHVzZXJu' + 'YW1lGiYKC0dldFVzZXJCeUlkEhcKB3VzZXJfaWQYASABKANSBnVzZXJJZBotChJHZXRQcmVrZX' + 'lzQnlVc2VySWQSFwoHdXNlcl9pZBgBIAEoA1IGdXNlcklkGiIKDkdldFVwbG9hZFRva2VuEhAK' + 'A2xlbhgBIAEoDVIDbGVuGlsKClVwbG9hZERhdGESIQoMdXBsb2FkX3Rva2VuGAEgASgMUgt1cG' + 'xvYWRUb2tlbhIWCgZvZmZzZXQYAiABKA1SBm9mZnNldBISCgRkYXRhGAMgASgMUgRkYXRhQhEK' + 'D0FwcGxpY2F0aW9uRGF0YQ=='); @$core.Deprecated('Use responseDescriptor instead') const Response$json = { diff --git a/lib/src/proto/api/server_to_client.pb.dart b/lib/src/proto/api/server_to_client.pb.dart index b73ca0c..f5e0b43 100644 --- a/lib/src/proto/api/server_to_client.pb.dart +++ b/lib/src/proto/api/server_to_client.pb.dart @@ -197,11 +197,15 @@ class V0 extends $pb.GeneratedMessage { class NewMessage extends $pb.GeneratedMessage { factory NewMessage({ $core.List<$core.int>? body, + $fixnum.Int64? fromUserId, }) { final $result = create(); if (body != null) { $result.body = body; } + if (fromUserId != null) { + $result.fromUserId = fromUserId; + } return $result; } NewMessage._() : super(); @@ -210,6 +214,7 @@ class NewMessage extends $pb.GeneratedMessage { static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'NewMessage', package: const $pb.PackageName(_omitMessageNames ? '' : 'server_to_client'), createEmptyInstance: create) ..a<$core.List<$core.int>>(1, _omitFieldNames ? '' : 'body', $pb.PbFieldType.OY) + ..aInt64(2, _omitFieldNames ? '' : 'fromUserId') ..hasRequiredFields = false ; @@ -242,6 +247,15 @@ class NewMessage extends $pb.GeneratedMessage { $core.bool hasBody() => $_has(0); @$pb.TagNumber(1) void clearBody() => clearField(1); + + @$pb.TagNumber(2) + $fixnum.Int64 get fromUserId => $_getI64(1); + @$pb.TagNumber(2) + set fromUserId($fixnum.Int64 v) { $_setInt64(1, v); } + @$pb.TagNumber(2) + $core.bool hasFromUserId() => $_has(1); + @$pb.TagNumber(2) + void clearFromUserId() => clearField(2); } class Response_PreKey extends $pb.GeneratedMessage { @@ -316,6 +330,7 @@ class Response_UserData extends $pb.GeneratedMessage { $core.List<$core.int>? signedPrekey, $core.List<$core.int>? signedPrekeySignature, $fixnum.Int64? signedPrekeyId, + $core.List<$core.int>? username, }) { final $result = create(); if (userId != null) { @@ -336,6 +351,9 @@ class Response_UserData extends $pb.GeneratedMessage { if (signedPrekeyId != null) { $result.signedPrekeyId = signedPrekeyId; } + if (username != null) { + $result.username = username; + } return $result; } Response_UserData._() : super(); @@ -349,6 +367,7 @@ class Response_UserData extends $pb.GeneratedMessage { ..a<$core.List<$core.int>>(4, _omitFieldNames ? '' : 'signedPrekey', $pb.PbFieldType.OY) ..a<$core.List<$core.int>>(5, _omitFieldNames ? '' : 'signedPrekeySignature', $pb.PbFieldType.OY) ..aInt64(6, _omitFieldNames ? '' : 'signedPrekeyId') + ..a<$core.List<$core.int>>(7, _omitFieldNames ? '' : 'username', $pb.PbFieldType.OY) ..hasRequiredFields = false ; @@ -420,6 +439,15 @@ class Response_UserData extends $pb.GeneratedMessage { $core.bool hasSignedPrekeyId() => $_has(5); @$pb.TagNumber(6) void clearSignedPrekeyId() => clearField(6); + + @$pb.TagNumber(7) + $core.List<$core.int> get username => $_getN(6); + @$pb.TagNumber(7) + set username($core.List<$core.int> v) { $_setBytes(6, v); } + @$pb.TagNumber(7) + $core.bool hasUsername() => $_has(6); + @$pb.TagNumber(7) + void clearUsername() => clearField(7); } enum Response_Ok_Ok { diff --git a/lib/src/proto/api/server_to_client.pbjson.dart b/lib/src/proto/api/server_to_client.pbjson.dart index 6ac2e9b..63a8c74 100644 --- a/lib/src/proto/api/server_to_client.pbjson.dart +++ b/lib/src/proto/api/server_to_client.pbjson.dart @@ -54,13 +54,15 @@ final $typed_data.Uint8List v0Descriptor = $convert.base64Decode( const NewMessage$json = { '1': 'NewMessage', '2': [ + {'1': 'from_user_id', '3': 2, '4': 1, '5': 3, '10': 'fromUserId'}, {'1': 'body', '3': 1, '4': 1, '5': 12, '10': 'body'}, ], }; /// Descriptor for `NewMessage`. Decode as a `google.protobuf.DescriptorProto`. final $typed_data.Uint8List newMessageDescriptor = $convert.base64Decode( - 'CgpOZXdNZXNzYWdlEhIKBGJvZHkYASABKAxSBGJvZHk='); + 'CgpOZXdNZXNzYWdlEiAKDGZyb21fdXNlcl9pZBgCIAEoA1IKZnJvbVVzZXJJZBISCgRib2R5GA' + 'EgASgMUgRib2R5'); @$core.Deprecated('Use responseDescriptor instead') const Response$json = { @@ -90,12 +92,14 @@ const Response_UserData$json = { '2': [ {'1': 'user_id', '3': 1, '4': 1, '5': 3, '10': 'userId'}, {'1': 'prekeys', '3': 2, '4': 3, '5': 11, '6': '.server_to_client.Response.PreKey', '10': 'prekeys'}, - {'1': 'public_identity_key', '3': 3, '4': 1, '5': 12, '9': 0, '10': 'publicIdentityKey', '17': true}, - {'1': 'signed_prekey', '3': 4, '4': 1, '5': 12, '9': 1, '10': 'signedPrekey', '17': true}, - {'1': 'signed_prekey_signature', '3': 5, '4': 1, '5': 12, '9': 2, '10': 'signedPrekeySignature', '17': true}, - {'1': 'signed_prekey_id', '3': 6, '4': 1, '5': 3, '9': 3, '10': 'signedPrekeyId', '17': true}, + {'1': 'username', '3': 7, '4': 1, '5': 12, '9': 0, '10': 'username', '17': true}, + {'1': 'public_identity_key', '3': 3, '4': 1, '5': 12, '9': 1, '10': 'publicIdentityKey', '17': true}, + {'1': 'signed_prekey', '3': 4, '4': 1, '5': 12, '9': 2, '10': 'signedPrekey', '17': true}, + {'1': 'signed_prekey_signature', '3': 5, '4': 1, '5': 12, '9': 3, '10': 'signedPrekeySignature', '17': true}, + {'1': 'signed_prekey_id', '3': 6, '4': 1, '5': 3, '9': 4, '10': 'signedPrekeyId', '17': true}, ], '8': [ + {'1': '_username'}, {'1': '_public_identity_key'}, {'1': '_signed_prekey'}, {'1': '_signed_prekey_signature'}, @@ -122,16 +126,17 @@ const Response_Ok$json = { final $typed_data.Uint8List responseDescriptor = $convert.base64Decode( 'CghSZXNwb25zZRIvCgJvaxgBIAEoCzIdLnNlcnZlcl90b19jbGllbnQuUmVzcG9uc2UuT2tIAF' 'ICb2sSKAoFZXJyb3IYAiABKA4yEC5lcnJvci5FcnJvckNvZGVIAFIFZXJyb3IaMAoGUHJlS2V5' - 'Eg4KAmlkGAEgASgDUgJpZBIWCgZwcmVrZXkYAiABKAxSBnByZWtleRqGAwoIVXNlckRhdGESFw' + 'Eg4KAmlkGAEgASgDUgJpZBIWCgZwcmVrZXkYAiABKAxSBnByZWtleRq0AwoIVXNlckRhdGESFw' 'oHdXNlcl9pZBgBIAEoA1IGdXNlcklkEjsKB3ByZWtleXMYAiADKAsyIS5zZXJ2ZXJfdG9fY2xp' - 'ZW50LlJlc3BvbnNlLlByZUtleVIHcHJla2V5cxIzChNwdWJsaWNfaWRlbnRpdHlfa2V5GAMgAS' - 'gMSABSEXB1YmxpY0lkZW50aXR5S2V5iAEBEigKDXNpZ25lZF9wcmVrZXkYBCABKAxIAVIMc2ln' - 'bmVkUHJla2V5iAEBEjsKF3NpZ25lZF9wcmVrZXlfc2lnbmF0dXJlGAUgASgMSAJSFXNpZ25lZF' - 'ByZWtleVNpZ25hdHVyZYgBARItChBzaWduZWRfcHJla2V5X2lkGAYgASgDSANSDnNpZ25lZFBy' - 'ZWtleUlkiAEBQhYKFF9wdWJsaWNfaWRlbnRpdHlfa2V5QhAKDl9zaWduZWRfcHJla2V5QhoKGF' - '9zaWduZWRfcHJla2V5X3NpZ25hdHVyZUITChFfc2lnbmVkX3ByZWtleV9pZBrBAQoCT2sSFAoE' - 'Tm9uZRgBIAEoCEgAUgROb25lEhgKBnVzZXJpZBgCIAEoA0gAUgZ1c2VyaWQSHgoJY2hhbGxlbm' - 'dlGAMgASgMSABSCWNoYWxsZW5nZRIiCgt1cGxvYWR0b2tlbhgEIAEoDEgAUgt1cGxvYWR0b2tl' - 'bhJBCgh1c2VyZGF0YRgFIAEoCzIjLnNlcnZlcl90b19jbGllbnQuUmVzcG9uc2UuVXNlckRhdG' - 'FIAFIIdXNlcmRhdGFCBAoCT2tCCgoIUmVzcG9uc2U='); + 'ZW50LlJlc3BvbnNlLlByZUtleVIHcHJla2V5cxIfCgh1c2VybmFtZRgHIAEoDEgAUgh1c2Vybm' + 'FtZYgBARIzChNwdWJsaWNfaWRlbnRpdHlfa2V5GAMgASgMSAFSEXB1YmxpY0lkZW50aXR5S2V5' + 'iAEBEigKDXNpZ25lZF9wcmVrZXkYBCABKAxIAlIMc2lnbmVkUHJla2V5iAEBEjsKF3NpZ25lZF' + '9wcmVrZXlfc2lnbmF0dXJlGAUgASgMSANSFXNpZ25lZFByZWtleVNpZ25hdHVyZYgBARItChBz' + 'aWduZWRfcHJla2V5X2lkGAYgASgDSARSDnNpZ25lZFByZWtleUlkiAEBQgsKCV91c2VybmFtZU' + 'IWChRfcHVibGljX2lkZW50aXR5X2tleUIQCg5fc2lnbmVkX3ByZWtleUIaChhfc2lnbmVkX3By' + 'ZWtleV9zaWduYXR1cmVCEwoRX3NpZ25lZF9wcmVrZXlfaWQawQEKAk9rEhQKBE5vbmUYASABKA' + 'hIAFIETm9uZRIYCgZ1c2VyaWQYAiABKANIAFIGdXNlcmlkEh4KCWNoYWxsZW5nZRgDIAEoDEgA' + 'UgljaGFsbGVuZ2USIgoLdXBsb2FkdG9rZW4YBCABKAxIAFILdXBsb2FkdG9rZW4SQQoIdXNlcm' + 'RhdGEYBSABKAsyIy5zZXJ2ZXJfdG9fY2xpZW50LlJlc3BvbnNlLlVzZXJEYXRhSABSCHVzZXJk' + 'YXRhQgQKAk9rQgoKCFJlc3BvbnNl'); diff --git a/lib/src/providers/api_provider.dart b/lib/src/providers/api_provider.dart index d05da02..3f46fd4 100644 --- a/lib/src/providers/api_provider.dart +++ b/lib/src/providers/api_provider.dart @@ -1,9 +1,13 @@ import 'dart:collection'; +import 'dart:convert'; import 'dart:math'; import 'package:fixnum/fixnum.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart'; import 'package:logging/logging.dart'; +import 'package:twonly/src/model/contacts_model.dart'; +import 'package:twonly/src/model/json/message.dart'; import 'package:twonly/src/proto/api/client_to_server.pb.dart' as client; import 'package:twonly/src/proto/api/client_to_server.pbserver.dart'; import 'package:twonly/src/proto/api/error.pb.dart'; @@ -127,7 +131,6 @@ class ApiProvider { messagesV0[msg.v0.seq] = msg; } else { _handleServerMessage(msg); - log.shout("Got a new message from the server: $msg"); } } catch (e) { log.shout("Error parsing the servers message: $e"); @@ -137,7 +140,7 @@ class ApiProvider { Future _handleServerMessage(server.ServerToClient msg) async { client.Response? response; - if (msg.v0.requestNewPreKeys) { + if (msg.v0.hasRequestNewPreKeys()) { List localPreKeys = await SignalHelper.getPreKeys(); List prekeysList = []; @@ -149,10 +152,27 @@ class ApiProvider { var prekeys = client.Response_Prekeys(prekeys: prekeysList); var ok = client.Response_Ok()..prekeys = prekeys; response = client.Response()..ok = ok; + } else if (msg.v0.hasNewMessage()) { + Uint8List body = Uint8List.fromList(msg.v0.newMessage.body); + Int64 fromUserId = msg.v0.newMessage.fromUserId; + Message? message = await SignalHelper.getDecryptedText(fromUserId, body); + if (message != null) { + Result username = await getUsername(fromUserId); + if (username.isSuccess) { + print(username.value); + Uint8List name = username.value.userdata.username; + DbContacts.insertNewContact( + utf8.decode(name), fromUserId.toInt(), true); + print(message); + } + } + var ok = client.Response_Ok()..none = true; + response = client.Response()..ok = ok; + } else { + log.shout("Got a new message from the server: $msg"); + return; } - if (response == null) return; - var v0 = client.V0() ..seq = msg.v0.seq ..response = response; @@ -308,8 +328,7 @@ class ApiProvider { Future register(String username, String? inviteCode) async { final signalIdentity = await SignalHelper.getSignalIdentity(); if (signalIdentity == null) { - return Result.error( - "There was an fatal error. Try reinstalling the app."); + return Result.error(ErrorCode.InternalError); } final signalStore = @@ -336,7 +355,18 @@ class ApiProvider { final resp = await _sendRequestV0(req); if (resp == null) { - return Result.error("Server is not reachable!"); + return Result.error(ErrorCode.InternalError); + } + return _asResult(resp); + } + + Future getUsername(Int64 userId) async { + var get = ApplicationData_GetUserById()..userId = userId; + var appData = ApplicationData()..getuserbyid = get; + var req = createClientToServerFromApplicationData(appData); + final resp = await _sendRequestV0(req); + if (resp == null) { + return Result.error(ErrorCode.InternalError); } return _asResult(resp); } @@ -348,7 +378,22 @@ class ApiProvider { final resp = await _sendRequestV0(req); if (resp == null) { - return Result.error("Server is not reachable!"); + return Result.error(ErrorCode.InternalError); + } + return _asResult(resp); + } + + Future sendTextMessage(Int64 target, Uint8List msg) async { + var testMessage = ApplicationData_TextMessage() + ..userId = target + ..body = msg; + + var appData = ApplicationData()..textmessage = testMessage; + var req = createClientToServerFromApplicationData(appData); + + final resp = await _sendRequestV0(req); + if (resp == null) { + return Result.error(ErrorCode.InternalError); } return _asResult(resp); } diff --git a/lib/src/providers/db_provider.dart b/lib/src/providers/db_provider.dart index 11a231a..4e10685 100644 --- a/lib/src/providers/db_provider.dart +++ b/lib/src/providers/db_provider.dart @@ -40,7 +40,7 @@ class DbProvider { await _createDb(db); }, onUpgrade: (db, oldVersion, newVersion) async { if (oldVersion < kVersion1) { - await _createDb(db); + //await _createDb(db); } }); } diff --git a/lib/src/signal/connect_pre_key_store.dart b/lib/src/signal/connect_pre_key_store.dart index bd1bb9c..d0611fe 100644 --- a/lib/src/signal/connect_pre_key_store.dart +++ b/lib/src/signal/connect_pre_key_store.dart @@ -37,7 +37,8 @@ class ConnectPreKeyStore extends PreKeyStore { @override Future storePreKey(int preKeyId, PreKeyRecord record) async { - if (await containsPreKey(preKeyId)) { + if (!await containsPreKey(preKeyId)) { + print(preKeyId); await dbProvider.db!.insert(DB.tableName, {DB.columnPreKeyId: preKeyId, DB.columnPreKey: record.serialize()}); } else { diff --git a/lib/src/signal/connect_session_store.dart b/lib/src/signal/connect_session_store.dart index d636ed1..1b5477d 100644 --- a/lib/src/signal/connect_session_store.dart +++ b/lib/src/signal/connect_session_store.dart @@ -57,7 +57,7 @@ class ConnectSessionStore extends SessionStore { @override Future storeSession( SignalProtocolAddress address, SessionRecord record) async { - if (await containsSession(address)) { + if (!await containsSession(address)) { await dbProvider.db!.insert(DB.tableName, { DB.columnDeviceId: address.getDeviceId(), DB.columnName: address.getName(), diff --git a/lib/src/signal/signal_helper.dart b/lib/src/signal/signal_helper.dart deleted file mode 100644 index bee189f..0000000 --- a/lib/src/signal/signal_helper.dart +++ /dev/null @@ -1,86 +0,0 @@ -import 'dart:convert'; -import 'dart:developer'; -import 'dart:typed_data'; -import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart'; -import 'package:twonly/src/utils/signal.dart'; -import 'connect_sender_key_store.dart'; -import 'connect_signal_protocol_store.dart'; - -class SignalDataModel { - Uint8List userId; - ConnectSignalProtocolStore signalStore; - ConnectSenderKeyStore senderKeyStore; - SignalDataModel({ - required this.userId, - required this.senderKeyStore, - required this.signalStore, - }); - // Session validation - Future generateSessionFingerPrint(String target) async { - try { - IdentityKey? targetIdentity = await signalStore - .getIdentity(SignalProtocolAddress(target, defaultDeviceId)); - if (targetIdentity != null) { - final generator = NumericFingerprintGenerator(5200); - final localFingerprint = generator.createFor( - 1, - userId, - (await signalStore.getIdentityKeyPair()).getPublicKey(), - Uint8List.fromList(utf8.encode(target)), - targetIdentity, - ); - return localFingerprint; - } - return null; - } catch (e) { - return null; - } - } - - // PreKeyBundle preKeyBundleFromJson(Map remoteBundle) { - - // } - - Future getEncryptedText(String text, String target) async { - try { - SessionCipher session = SessionCipher.fromStore( - signalStore, SignalProtocolAddress(target, defaultDeviceId)); - final ciphertext = - await session.encrypt(Uint8List.fromList(utf8.encode(text))); - Map data = { - "msg": base64Encode(ciphertext.serialize()), - "type": ciphertext.getType(), - }; - return jsonEncode(data); - } catch (e) { - log(e.toString()); - return null; - } - } - - Future getDecryptedText(String source, String msg) async { - try { - SessionCipher session = SessionCipher.fromStore( - signalStore, SignalProtocolAddress(source, defaultDeviceId)); - Map data = jsonDecode(msg); - if (data["type"] == CiphertextMessage.prekeyType) { - PreKeySignalMessage pre = - PreKeySignalMessage(base64Decode(data["msg"])); - Uint8List plaintext = await session.decrypt(pre); - String dectext = utf8.decode(plaintext); - return dectext; - } else if (data["type"] == CiphertextMessage.whisperType) { - SignalMessage signalMsg = - SignalMessage.fromSerialized(base64Decode(data["msg"])); - Uint8List plaintext = await session.decryptFromSignal(signalMsg); - String dectext = utf8.decode(plaintext); - return dectext; - } else { - return null; - } - } catch (e) { - log(e.toString()); - return null; - } - } -} diff --git a/lib/src/utils/api.dart b/lib/src/utils/api.dart index 28a9671..595fc6c 100644 --- a/lib/src/utils/api.dart +++ b/lib/src/utils/api.dart @@ -1,7 +1,10 @@ import 'dart:convert'; +import 'package:flutter/foundation.dart'; import 'package:logging/logging.dart'; import 'package:twonly/main.dart'; import 'package:twonly/src/model/contacts_model.dart'; +import 'package:twonly/src/model/json/message.dart'; +import 'package:twonly/src/proto/api/error.pb.dart'; import 'package:twonly/src/providers/api_provider.dart'; import 'package:twonly/src/utils/misc.dart'; // ignore: library_prefixes @@ -12,11 +15,30 @@ Future addNewContact(String username) async { final res = await apiProvider.getUserData(username); if (res.isSuccess) { + bool added = await DbContacts.insertNewContact( + username, res.value.userdata.userId.toInt(), false); + + if (!added) { + print("RETURN FALSE HIER!!!"); + // return false; + } + if (await SignalHelper.addNewContact(res.value.userdata)) { - await dbProvider.db!.insert(DbContacts.tableName, { - DbContacts.columnDisplayName: username, - DbContacts.columnUserId: res.value.userdata.userId.toInt() - }); + Message msg = + Message(kind: MessageKind.contactRequest, timestamp: DateTime.now()); + + Uint8List? bytes = + await SignalHelper.encryptMessage(msg, res.value.userdata.userId); + + if (bytes == null) { + Logger("utils/api").shout("Error encryption message!"); + return res.error(ErrorCode.InternalError); + } + + Result resp = + await apiProvider.sendTextMessage(res.value.userdata.userId, bytes); + + return resp.isSuccess; } } return res.isSuccess; diff --git a/lib/src/utils/signal.dart b/lib/src/utils/signal.dart index 414a97e..65c36cf 100644 --- a/lib/src/utils/signal.dart +++ b/lib/src/utils/signal.dart @@ -1,8 +1,11 @@ import 'dart:convert'; +import 'dart:ffi'; +import 'dart:io'; import 'dart:typed_data'; import 'package:fixnum/fixnum.dart'; import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart'; import 'package:logging/logging.dart'; +import 'package:twonly/src/model/json/message.dart'; import 'package:twonly/src/model/json/signal_identity.dart'; import 'package:twonly/src/proto/api/server_to_client.pb.dart'; import 'package:twonly/src/signal/connect_signal_protocol_store.dart'; @@ -86,13 +89,17 @@ Future getSignalStore() async { } Future getSignalIdentity() async { - final storage = getSecureStorage(); - final signalIdentityJson = await storage.read(key: "signal_identity"); - if (signalIdentityJson == null) { + try { + final storage = getSecureStorage(); + final signalIdentityJson = await storage.read(key: "signal_identity"); + if (signalIdentityJson == null) { + return null; + } + return SignalIdentity.fromJson(jsonDecode(signalIdentityJson)); + } catch (e) { + Logger("signal.dart/getSignalIdentity").shout(e); return null; } - - return SignalIdentity.fromJson(jsonDecode(signalIdentityJson)); } Future getSignalStoreFromIdentity( @@ -140,3 +147,99 @@ Future createIfNotExistsSignalIdentity() async { await storage.write( key: "signal_identity", value: jsonEncode(storedSignalIdentity)); } + +// Future generateSessionFingerPrint(String target) async { +// try { +// IdentityKey? targetIdentity = await signalStore +// .getIdentity(SignalProtocolAddress(target, defaultDeviceId)); +// if (targetIdentity != null) { +// final generator = NumericFingerprintGenerator(5200); +// final localFingerprint = generator.createFor( +// 1, +// userId, +// (await signalStore.getIdentityKeyPair()).getPublicKey(), +// Uint8List.fromList(utf8.encode(target)), +// targetIdentity, +// ); +// return localFingerprint; +// } +// return null; +// } catch (e) { +// return null; +// } +// } + +Uint8List intToBytes(int value) { + final byteData = ByteData(4); + byteData.setInt32(0, value, Endian.big); + return byteData.buffer.asUint8List(); +} + +int bytesToInt(Uint8List bytes) { + final byteData = ByteData.sublistView(bytes); + return byteData.getInt32(0, Endian.big); +} + +List? removeLastFourBytes(Uint8List original) { + if (original.length < 4) { + return null; + } + final newList = Uint8List(original.length - 4); + newList.setAll(0, original.sublist(0, original.length - 4)); + + final lastFourBytes = original.sublist(original.length - 4); + return [newList, lastFourBytes]; +} + +Future encryptMessage(Message msg, Int64 target) async { + try { + ConnectSignalProtocolStore signalStore = (await getSignalStore())!; + + SessionCipher session = SessionCipher.fromStore( + signalStore, SignalProtocolAddress(target.toString(), defaultDeviceId)); + + final ciphertext = await session + .encrypt(Uint8List.fromList(gzip.encode(utf8.encode(msg.toJson())))); + + var b = BytesBuilder(); + b.add(ciphertext.serialize()); + b.add(intToBytes(ciphertext.getType())); + + return b.takeBytes(); + } catch (e) { + Logger("utils/signal").shout(e.toString()); + return null; + } +} + +Future getDecryptedText(Int64 source, Uint8List msg) async { + try { + ConnectSignalProtocolStore signalStore = (await getSignalStore())!; + + SessionCipher session = SessionCipher.fromStore( + signalStore, SignalProtocolAddress(source.toString(), defaultDeviceId)); + + List? msgs = removeLastFourBytes(msg); + if (msgs == null) return null; + Uint8List body = msgs[0]; + int type = bytesToInt(msgs[1]); + + // gzip.decode(body); + + Uint8List plaintext; + if (type == CiphertextMessage.prekeyType) { + PreKeySignalMessage pre = PreKeySignalMessage(body); + plaintext = await session.decrypt(pre); + } else if (type == CiphertextMessage.whisperType) { + SignalMessage signalMsg = SignalMessage.fromSerialized(body); + plaintext = await session.decryptFromSignal(signalMsg); + } else { + return null; + } + Message dectext = Message.fromJson(utf8.decode(gzip.decode(plaintext))); + return dectext; + } catch (e) { + Logger("utils/signal").shout(e.toString()); + return null; + } +} diff --git a/lib/src/views/chat_list_view.dart b/lib/src/views/chat_list_view.dart index 1a41078..05ee2c1 100644 --- a/lib/src/views/chat_list_view.dart +++ b/lib/src/views/chat_list_view.dart @@ -1,4 +1,5 @@ import 'package:twonly/src/components/initialsavatar_component.dart'; +import 'package:twonly/src/model/contacts_model.dart'; import 'package:twonly/src/views/search_username_view.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'new_message_view.dart'; @@ -56,12 +57,19 @@ class ChatListView extends StatefulWidget { class _ChatListViewState extends State { int _secondsSinceOpen = 0; + int _newContactRequests = 0; late Timer _timer; @override void initState() { super.initState(); _startTimer(); + _checkNewContactRequests(); + } + + Future _checkNewContactRequests() async { + _newContactRequests = (await DbContacts.getUsers()).length; + setState(() {}); } void _startTimer() { @@ -172,17 +180,41 @@ class _ChatListViewState extends State { appBar: AppBar( 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(), + Stack( + children: [ + IconButton( + icon: Icon(Icons.person_add), // User with add icon + onPressed: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => SearchUsernameView(), + ), + ); + }, + ), + if (_newContactRequests > 0) + Positioned( + right: 5, + top: 0, + child: Container( + padding: EdgeInsets.all(5.0), // Add some padding + decoration: BoxDecoration( + color: Colors.red, // Background color + shape: BoxShape.circle, // Make it circular + ), + child: Center( + child: Text( + _newContactRequests.toString(), + style: TextStyle( + color: Colors.white, // Text color + fontSize: 10), + ), + ), + ), ), - ); - }, - ), + ], + ) ], ), body: ListView.builder( diff --git a/lib/src/views/search_username_view.dart b/lib/src/views/search_username_view.dart index 1efcd0a..de67443 100644 --- a/lib/src/views/search_username_view.dart +++ b/lib/src/views/search_username_view.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:logging/logging.dart'; +import 'package:twonly/src/model/contacts_model.dart'; import 'package:twonly/src/utils/api.dart'; import 'package:twonly/src/views/register_view.dart'; @@ -14,28 +15,24 @@ class SearchUsernameView extends StatefulWidget { class _SearchUsernameView extends State { final TextEditingController searchUserName = TextEditingController(); - bool _isLoading = false; Future _addNewUser(BuildContext context) async { - Timer timer = Timer(Duration(milliseconds: 500), () { - setState(() { - _isLoading = true; - }); + setState(() { + _isLoading = true; }); final status = await addNewContact(searchUserName.text); - timer.cancel(); - // loaderDelay.timeout(Duration(microseconds: 0)); setState(() { _isLoading = false; }); + Logger("search_user_name").warning("Replace instead of pop"); if (context.mounted) { if (status) { - Navigator.pop(context); + // Navigator.pop(context); } else if (context.mounted) { showAlertDialog( context, @@ -84,7 +81,7 @@ class _SearchUsernameView extends State { controller: searchUserName, decoration: getInputDecoration( AppLocalizations.of(context)!.searchUsernameInput))), - const SizedBox(height: 40), + const SizedBox(height: 20), OutlinedButton.icon( icon: Icon(Icons.qr_code), onPressed: () { @@ -93,9 +90,18 @@ class _SearchUsernameView extends State { }, label: Text("QR-Code scannen"), ), - SizedBox(height: 20), - const SizedBox(height: 40), - if (_isLoading) const Center(child: CircularProgressIndicator()) + SizedBox(height: 30), + Container( + alignment: Alignment.centerLeft, + padding: EdgeInsets.symmetric(horizontal: 4.0, vertical: 10), + child: Text( + "Neue Followanfragen", + style: TextStyle(fontSize: 20), + ), + ), + Expanded( + child: ContactsListView(), + ) ], ), ), @@ -103,11 +109,73 @@ class _SearchUsernameView extends State { padding: const EdgeInsets.only(bottom: 30.0), child: FloatingActionButton( onPressed: () { - _addNewUser(context); + if (!_isLoading) _addNewUser(context); }, - child: const Icon(Icons.arrow_right_rounded), + child: (_isLoading) + ? const Center(child: CircularProgressIndicator()) + : Icon(Icons.arrow_right_rounded), ), ), ); } } + +class ContactsListView extends StatefulWidget { + @override + State createState() => _ContactsListViewState(); +} + +class _ContactsListViewState extends State { + List _allContacts = []; + + @override + void initState() { + super.initState(); + _loadContacts(); + } + + Future _loadContacts() async { + List allContacts = await DbContacts.getUsers(); + _allContacts = allContacts.where((contact) => !contact.accepted).toList(); + } + + @override + Widget build(BuildContext context) { + return ListView.builder( + itemCount: _allContacts.length, + itemBuilder: (context, index) { + final contact = _allContacts[index]; + + if (!contact.requested) { + return ListTile( + title: Text(contact.displayName), + subtitle: Text('Pending'), + ); + } + + return ListTile( + title: Text(contact.displayName), + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + IconButton( + icon: Icon(Icons.close, color: Colors.red), + onPressed: () { + // Handle reject action + print('Rejected ${contact.displayName}'); + }, + ), + IconButton( + icon: Icon(Icons.check, color: Colors.green), + onPressed: () { + // Handle accept action + print('Accepted ${contact.displayName}'); + }, + ), + ], + ), + ); + }, + ); + } +}