follow request

This commit is contained in:
otsmr 2025-01-25 03:30:41 +01:00
parent b06c2e1cc8
commit 20c20eb1e1
19 changed files with 533 additions and 197 deletions

View file

@ -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) {
// 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);

View file

@ -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.",

View file

@ -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<String>(columnDisplayName);
static const columnAccepted = "accepted";
final accepted = CvField<int>(columnAccepted);
static const columnRequested = "requested";
final requested = CvField<int>(columnRequested);
static const columnCreatedAt = "created_at";
final createdAt = CvField<DateTime>(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
)
""";
}
@override
List<CvField> get fields =>
[userId, displayName, accepted, requested, createdAt];
static Future<List<Contact>> getUsers() async {
var users = await dbProvider.db!.query(tableName,
columns: [columnUserId, columnDisplayName, columnCreatedAt]);
try {
var users = await dbProvider.db!.query(tableName, columns: [
columnUserId,
columnDisplayName,
columnAccepted,
columnRequested,
columnCreatedAt
]);
if (users.isEmpty) return [];
List<Contact> parsedUsers = [];
for (int i = 0; i < users.length; i++) {
parsedUsers.add(Contact(
print(users[i]);
parsedUsers.add(
Contact(
userId: Int64(users.cast()[i][columnUserId]),
displayName: users.cast()[i][columnDisplayName]));
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 [];
}
}
@override
List<CvField> get fields => [userId, createdAt];
static Future<bool> 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;
}
}
}

View file

@ -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<String, dynamic> json) {
static Message fromJson(String jsonString) {
Map<String, dynamic> 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<String, dynamic> toJson(Message instance) {
String toJson() {
var json = <String, dynamic>{
'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);
}
}

View file

@ -22,7 +22,6 @@ const _$MessageKindEnumMap = {
};
Message _$MessageFromJson(Map<String, dynamic> 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<String, dynamic> json) => Message(
);
Map<String, dynamic> _$MessageToJson(Message instance) => <String, dynamic>{
'fromUserId': const Int64Converter().toJson(instance.fromUserId),
'kind': _$MessageKindEnumMap[instance.kind]!,
'content': instance.content,
'timestamp': instance.timestamp.toIso8601String(),

View file

@ -1,5 +1,5 @@
const String dbName = 'twonly.db';
const int kVersion1 = 1;
const int kVersion1 = 3;
String tableLibSignal = 'LibSignal';

View file

@ -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<ApplicationData_GetUserById> createRepeated() => $pb.PbList<ApplicationData_GetUserById>();
@$core.pragma('dart2js:noInline')
static ApplicationData_GetUserById getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<ApplicationData_GetUserById>(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<ApplicationData_TextMessage>(1, _omitFieldNames ? '' : 'textmessage', subBuilder: ApplicationData_TextMessage.create)
..aOM<ApplicationData_GetUserByUsername>(2, _omitFieldNames ? '' : 'getuserbyusername', subBuilder: ApplicationData_GetUserByUsername.create)
..aOM<ApplicationData_GetPrekeysByUserId>(3, _omitFieldNames ? '' : 'getprekeysbyuserid', subBuilder: ApplicationData_GetPrekeysByUserId.create)
..aOM<ApplicationData_GetUploadToken>(4, _omitFieldNames ? '' : 'getuploadtoken', subBuilder: ApplicationData_GetUploadToken.create)
..aOM<ApplicationData_UploadData>(5, _omitFieldNames ? '' : 'uploaddata', subBuilder: ApplicationData_UploadData.create)
..aOM<ApplicationData_GetUserById>(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 {

View file

@ -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 = {

View file

@ -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 {

View file

@ -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');

View file

@ -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<PreKeyRecord> localPreKeys = await SignalHelper.getPreKeys();
List<client.Response_PreKey> prekeysList = [];
@ -149,9 +152,26 @@ 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
@ -308,8 +328,7 @@ class ApiProvider {
Future<Result> 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<Result> 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<Result> 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);
}

View file

@ -40,7 +40,7 @@ class DbProvider {
await _createDb(db);
}, onUpgrade: (db, oldVersion, newVersion) async {
if (oldVersion < kVersion1) {
await _createDb(db);
//await _createDb(db);
}
});
}

View file

@ -37,7 +37,8 @@ class ConnectPreKeyStore extends PreKeyStore {
@override
Future<void> 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 {

View file

@ -57,7 +57,7 @@ class ConnectSessionStore extends SessionStore {
@override
Future<void> 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(),

View file

@ -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<Fingerprint?> 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<String, dynamic> remoteBundle) {
// }
Future<String?> 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<String, dynamic> data = {
"msg": base64Encode(ciphertext.serialize()),
"type": ciphertext.getType(),
};
return jsonEncode(data);
} catch (e) {
log(e.toString());
return null;
}
}
Future<String?> 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;
}
}
}

View file

@ -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<bool> 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;

View file

@ -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<ConnectSignalProtocolStore?> getSignalStore() async {
}
Future<SignalIdentity?> getSignalIdentity() async {
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;
}
}
Future<ConnectSignalProtocolStore> getSignalStoreFromIdentity(
@ -140,3 +147,99 @@ Future createIfNotExistsSignalIdentity() async {
await storage.write(
key: "signal_identity", value: jsonEncode(storedSignalIdentity));
}
// Future<Fingerprint?> 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<Uint8List>? 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<Uint8List?> 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<Message?> getDecryptedText(Int64 source, Uint8List msg) async {
try {
ConnectSignalProtocolStore signalStore = (await getSignalStore())!;
SessionCipher session = SessionCipher.fromStore(
signalStore, SignalProtocolAddress(source.toString(), defaultDeviceId));
List<Uint8List>? 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;
}
}

View file

@ -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<ChatListView> {
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,6 +180,8 @@ class _ChatListViewState extends State<ChatListView> {
appBar: AppBar(
title: Text(AppLocalizations.of(context)!.chatsTitle),
actions: [
Stack(
children: [
IconButton(
icon: Icon(Icons.person_add), // User with add icon
onPressed: () {
@ -183,6 +193,28 @@ class _ChatListViewState extends State<ChatListView> {
);
},
),
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(

View file

@ -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<SearchUsernameView> {
final TextEditingController searchUserName = TextEditingController();
bool _isLoading = false;
Future _addNewUser(BuildContext context) async {
Timer timer = Timer(Duration(milliseconds: 500), () {
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<SearchUsernameView> {
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<SearchUsernameView> {
},
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<SearchUsernameView> {
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<ContactsListView> createState() => _ContactsListViewState();
}
class _ContactsListViewState extends State<ContactsListView> {
List<Contact> _allContacts = [];
@override
void initState() {
super.initState();
_loadContacts();
}
Future _loadContacts() async {
List<Contact> 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}');
},
),
],
),
);
},
);
}
}