invitation work

This commit is contained in:
Christien Rioux 2023-08-04 01:00:38 -04:00
parent 95ed8b28a0
commit 7496a1a2a7
22 changed files with 591 additions and 47 deletions

View File

@ -29,6 +29,10 @@
"ok": "Ok",
"cancel": "Cancel"
},
"toast": {
"error": "Error",
"info": "Info"
},
"language": {
"name": {
"en": "English"
@ -68,12 +72,20 @@
"note_text": "Contact invitations can be used by anyone. Make sure you send the invitation to your contact over a secure medium, and preferably use a password or pin to ensure that they are the only ones who can unlock the invitation and accept it.",
"pin_description": "Choose a PIN to protect the contact invite.\n\nThis level of security is appropriate only for casual connections in public environments for 'shoulder surfing' protection.",
"password_description": "Choose a strong password to protect the contact invite.\n\nThis level of security is appropriate when you must be sure the contact invitation is only accepted by its intended recipient. Share this password over a different medium than the invite itself.",
"pin_does_not_match": "PIN does not match"
"pin_does_not_match": "PIN does not match",
"contact_invitation": "Contact Invitation",
"failed_to_generate": "Failed to generate contact invitation",
"copy_invitation": "Copy Invitation",
"invitation_copied": "Invitation Copied"
},
"enter_pin_dialog": {
"enter_pin": "Enter PIN",
"reenter_pin": "Re-Enter PIN To Confirm"
},
"contact_list": {
"search": "Search contacts",
"invitation": "Invitation"
},
"themes": {
"vapor": "Vapor"
}

View File

@ -11,6 +11,8 @@ PODS:
- shared_preferences_foundation (0.0.1):
- Flutter
- FlutterMacOS
- smart_auth (0.0.1):
- Flutter
- sqflite (0.0.3):
- Flutter
- FMDB (>= 2.7.5)
@ -26,6 +28,7 @@ DEPENDENCIES:
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
- share_plus (from `.symlinks/plugins/share_plus/ios`)
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
- smart_auth (from `.symlinks/plugins/smart_auth/ios`)
- sqflite (from `.symlinks/plugins/sqflite/ios`)
- system_info_plus (from `.symlinks/plugins/system_info_plus/ios`)
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
@ -44,6 +47,8 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/share_plus/ios"
shared_preferences_foundation:
:path: ".symlinks/plugins/shared_preferences_foundation/darwin"
smart_auth:
:path: ".symlinks/plugins/smart_auth/ios"
sqflite:
:path: ".symlinks/plugins/sqflite/ios"
system_info_plus:
@ -59,6 +64,7 @@ SPEC CHECKSUMS:
path_provider_foundation: 29f094ae23ebbca9d3d0cec13889cd9060c0e943
share_plus: 599aa54e4ea31d4b4c0e9c911bcc26c55e791028
shared_preferences_foundation: 5b919d13b803cadd15ed2dc053125c68730e5126
smart_auth: 4bedbc118723912d0e45a07e8ab34039c19e04f2
sqflite: 31f7eba61e3074736dff8807a9b41581e4f7f15a
system_info_plus: 5393c8da281d899950d751713575fbf91c7709aa
url_launcher_ios: 08a3dfac5fb39e8759aeb0abbd5d9480f30fc8b4

View File

@ -1,14 +1,17 @@
import 'dart:async';
import 'dart:ffi';
import 'package:archive/archive.dart';
import 'package:awesome_extensions/awesome_extensions.dart';
import 'package:basic_utils/basic_utils.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_animate/flutter_animate.dart';
import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_translate/flutter_translate.dart';
import 'package:qr_flutter/qr_flutter.dart';
import '../tools/tools.dart';
import '../veilid_support/veilid_support.dart';
class ContactInvitationDisplayDialog extends ConsumerStatefulWidget {
const ContactInvitationDisplayDialog({
@ -40,14 +43,14 @@ class ContactInvitationDisplayDialogState
extends ConsumerState<ContactInvitationDisplayDialog> {
final focusNode = FocusNode();
final formKey = GlobalKey<FormState>();
late final FutureProvider<Uint8List?> _generateFutureProvider;
late final AutoDisposeFutureProvider<Uint8List?> _generateFutureProvider;
@override
void initState() {
super.initState();
_generateFutureProvider =
FutureProvider<Uint8List>((ref) async => widget.generator);
AutoDisposeFutureProvider<Uint8List>((ref) async => widget.generator);
}
@override
@ -56,11 +59,24 @@ class ContactInvitationDisplayDialogState
super.dispose();
}
String makeTextInvite(String message, Uint8List data) {
final invite = StringUtils.addCharAtPosition(
base64UrlNoPadEncode(data), '\n', 40,
repeat: true);
final msg = message.isNotEmpty ? '$message\n' : '';
return '$msg'
'--- BEGIN VEILIDCHAT CONTACT INVITE ---\n'
'$invite\n'
'--- END VEILIDCHAT CONTACT INVITE ---\n';
}
@override
// ignore: prefer_expression_function_bodies
Widget build(BuildContext context) {
final theme = Theme.of(context);
final scale = theme.extension<ScaleScheme>()!;
final textTheme = theme.textTheme;
final signedContactInvitationBytesV = ref.watch(_generateFutureProvider);
final cardsize = MediaQuery.of(context).size.shortestSide - 24;
@ -71,17 +87,62 @@ class ContactInvitationDisplayDialogState
height: cardsize,
child: signedContactInvitationBytesV.when(
loading: () => waitingPage(context),
data: (data) => Form(
key: formKey,
child: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: [Text("Contact Invitation")])),
data: (data) {
if (data == null) {
Navigator.of(context).pop();
return const Text('');
}
final compressedData =
Uint8List.fromList(BZip2Encoder().encode(data));
return Form(
key: formKey,
child: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
FittedBox(
fit: BoxFit.scaleDown,
child: Text(
translate(
'send_invite_dialog.contact_invitation'),
style: textTheme.headlineMedium!
.copyWith(color: Colors.black))),
ConstrainedBox(
constraints: const BoxConstraints(
minWidth: 300, minHeight: 300),
child: QrImageView.withQr(
size: 300,
qr: QrCode.fromUint8List(
data: compressedData,
errorCorrectLevel:
QrErrorCorrectLevel.L))),
FittedBox(
fit: BoxFit.scaleDown,
child: Text(widget.message,
softWrap: true,
style: textTheme.headlineSmall!
.copyWith(color: Colors.black))),
ElevatedButton.icon(
icon: const Icon(Icons.copy),
label: Text(translate(
'send_invite_dialog.copy_invitation')),
onPressed: () async {
showInfoToast(
context,
translate(
'send_invite_dialog.invitation_copied'));
await Clipboard.setData(ClipboardData(
text: makeTextInvite(
widget.message, compressedData)));
},
),
]));
},
error: (e, s) {
Navigator.of(context).pop();
showErrorToast(
context, "Failed to generate contact invitation: $e");
return Text("");
showErrorToast(context,
translate('send_invite_dialog.failed_to_generate'));
return const Text('');
})));
}

View File

@ -0,0 +1,79 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_slidable/flutter_slidable.dart';
import 'package:flutter_translate/flutter_translate.dart';
import 'package:veilid/veilid.dart';
import '../../entities/proto.dart' as proto;
class ContactInvitationItemWidget extends ConsumerWidget {
const ContactInvitationItemWidget(
{required this.contactInvitationRecord, super.key});
final proto.ContactInvitationRecord contactInvitationRecord;
@override
// ignore: prefer_expression_function_bodies
Widget build(BuildContext context, WidgetRef ref) {
return Slidable(
// Specify a key if the Slidable is dismissible.
key: ObjectKey(contactInvitationRecord),
// The start action pane is the one at the left or the top side.
startActionPane: ActionPane(
// A motion is a widget used to control how the pane animates.
motion: const DrawerMotion(),
// A pane can dismiss the Slidable.
//dismissible: DismissiblePane(onDismissed: () {}),
// All actions are defined in the children parameter.
children: [
// A SlidableAction can have an icon and/or a label.
SlidableAction(
onPressed: (context) => (),
backgroundColor: Color(0xFFFE4A49),
foregroundColor: Colors.white,
icon: Icons.delete,
label: 'Delete',
),
SlidableAction(
onPressed: (context) => (),
backgroundColor: Color(0xFF21B7CA),
foregroundColor: Colors.white,
icon: Icons.edit,
label: 'Edit',
),
],
),
// The end action pane is the one at the right or the bottom side.
// endActionPane: ActionPane(
// motion: const DrawerMotion(),
// children: [
// SlidableAction(
// // An action can be bigger than the others.
// flex: 2,
// onPressed: (context) => (),
// backgroundColor: Color(0xFF7BC043),
// foregroundColor: Colors.white,
// icon: Icons.archive,
// label: 'Archive',
// ),
// SlidableAction(
// onPressed: (context) => (),
// backgroundColor: Color(0xFF0392CF),
// foregroundColor: Colors.white,
// icon: Icons.save,
// label: 'Save',
// ),
// ],
// ),
// The child of the Slidable is what the user sees when the
// component is not dragged.
child: ListTile(
title: Text(translate('contact_list.invitation')),
subtitle: Text(contactInvitationRecord.message),
//Text(Timestamp.fromInt64(contactInvitationRecord.expiration) / ),
leading: Icon(Icons.person_add)));
}
}

View File

@ -0,0 +1,56 @@
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_slidable/flutter_slidable.dart';
import 'package:flutter_translate/flutter_translate.dart';
import 'package:searchable_listview/searchable_listview.dart';
import '../../entities/proto.dart' as proto;
import '../tools/tools.dart';
import 'contact_invitation_item_widget.dart';
import 'contact_item_widget.dart';
import 'empty_contact_list_widget.dart';
class ContactInvitationListWidget extends ConsumerWidget {
const ContactInvitationListWidget(
{required this.contactInvitationRecordList, super.key});
final IList<proto.ContactInvitationRecord> contactInvitationRecordList;
@override
// ignore: prefer_expression_function_bodies
Widget build(BuildContext context, WidgetRef ref) {
final theme = Theme.of(context);
final textTheme = theme.textTheme;
//final scale = theme.extension<ScaleScheme>()!;
return Container(
decoration: BoxDecoration(
color: Theme.of(context).scaffoldBackgroundColor,
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'Contacts',
style: textTheme.bodyMedium,
),
ListView.builder(itemBuilder: (context, index) {
if (index < 0 || index >= contactInvitationRecordList.length) {
return null;
}
return ContactInvitationItemWidget(
contactInvitationRecord: contactInvitationRecordList[index],
key: ObjectKey(contactInvitationRecordList[index]));
}, findChildIndexCallback: (key) {
final index = contactInvitationRecordList.indexOf(
(key as ObjectKey).value! as proto.ContactInvitationRecord);
if (index == -1) {
return null;
}
return index;
})
],
),
);
}
}

View File

@ -0,0 +1,75 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_slidable/flutter_slidable.dart';
import '../../entities/proto.dart' as proto;
class ContactItemWidget extends ConsumerWidget {
const ContactItemWidget({required this.contact, super.key});
final proto.Contact contact;
@override
// ignore: prefer_expression_function_bodies
Widget build(BuildContext context, WidgetRef ref) {
return Slidable(
// Specify a key if the Slidable is dismissible.
key: ObjectKey(contact),
// The start action pane is the one at the left or the top side.
startActionPane: ActionPane(
// A motion is a widget used to control how the pane animates.
motion: const DrawerMotion(),
// A pane can dismiss the Slidable.
//dismissible: DismissiblePane(onDismissed: () {}),
// All actions are defined in the children parameter.
children: [
// A SlidableAction can have an icon and/or a label.
SlidableAction(
onPressed: (context) => (),
backgroundColor: Color(0xFFFE4A49),
foregroundColor: Colors.white,
icon: Icons.delete,
label: 'Delete',
),
SlidableAction(
onPressed: (context) => (),
backgroundColor: Color(0xFF21B7CA),
foregroundColor: Colors.white,
icon: Icons.edit,
label: 'Edit',
),
],
),
// The end action pane is the one at the right or the bottom side.
// endActionPane: ActionPane(
// motion: const DrawerMotion(),
// children: [
// SlidableAction(
// // An action can be bigger than the others.
// flex: 2,
// onPressed: (context) => (),
// backgroundColor: Color(0xFF7BC043),
// foregroundColor: Colors.white,
// icon: Icons.archive,
// label: 'Archive',
// ),
// SlidableAction(
// onPressed: (context) => (),
// backgroundColor: Color(0xFF0392CF),
// foregroundColor: Colors.white,
// icon: Icons.save,
// label: 'Save',
// ),
// ],
// ),
// The child of the Slidable is what the user sees when the
// component is not dragged.
child: ListTile(
title: Text(contact.editedProfile.name),
subtitle: Text(contact.editedProfile.title),
leading: Icon(Icons.person)));
}
}

View File

@ -1,20 +1,25 @@
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_slidable/flutter_slidable.dart';
import 'package:flutter_translate/flutter_translate.dart';
import 'package:searchable_listview/searchable_listview.dart';
import '../../entities/proto.dart' as proto;
import '../tools/tools.dart';
import 'contact_item_widget.dart';
import 'empty_contact_list_widget.dart';
class ContactListWidget extends ConsumerWidget {
const ContactListWidget({required this.contactList, super.key});
final List<proto.Contact> contactList;
final IList<proto.Contact> contactList;
@override
// ignore: prefer_expression_function_bodies
Widget build(BuildContext context, WidgetRef ref) {
//
if (contactList.isEmpty) {
return const EmptyContactListWidget();
}
final theme = Theme.of(context);
final textTheme = theme.textTheme;
//final scale = theme.extension<ScaleScheme>()!;
return Container(
decoration: BoxDecoration(
@ -23,16 +28,36 @@ class ContactListWidget extends ConsumerWidget {
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.group_add,
color: Theme.of(context).disabledColor,
size: 48,
),
Text(
'Contacts',
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Theme.of(context).disabledColor,
style: textTheme.bodyMedium,
),
SearchableList<proto.Contact>(
initialList: contactList.toList(),
builder: (contact) => ContactItemWidget(contact: contact),
filter: (value) {
final lowerValue = value.toLowerCase();
return contactList
.where((element) =>
element.editedProfile.name
.toLowerCase()
.contains(lowerValue) ||
element.editedProfile.title
.toLowerCase()
.contains(lowerValue))
.toList();
},
emptyWidget: const EmptyContactListWidget(),
inputDecoration: InputDecoration(
labelText: translate('contact_list.search'),
fillColor: Colors.white,
focusedBorder: OutlineInputBorder(
borderSide: const BorderSide(
color: Colors.blue,
),
borderRadius: BorderRadius.circular(10),
),
),
),
],
),

View File

@ -104,6 +104,7 @@ class SendInviteDialogState extends ConsumerState<SendInviteDialog> {
activeAccountInfo: activeAccountInfo,
encryptionKeyType: _encryptionKeyType,
encryptionKey: _encryptionKey,
message: _messageTextController.text,
expiration: _expiration);
// ignore: use_build_context_synchronously
if (!context.mounted) {
@ -119,6 +120,7 @@ class SendInviteDialogState extends ConsumerState<SendInviteDialog> {
// if (ret == null) {
// return;
// }
ref.invalidate(fetchContactInvitationRecordsProvider);
navigator.pop();
}

View File

@ -131,15 +131,22 @@ extension IdentityMasterExtension on IdentityMaster {
// Create new account to insert into identity
await (await pool.create(parent: identityRec.key))
.deleteScope((accountRec) async {
// Make empty contact list
final contactList =
await (await DHTShortArray.create(parent: accountRec.key))
.scope((r) => r.record.ownedDHTRecordPointer);
// Make empty contact invitation record list
final contactInvitationRecords = await (await DHTShortArray.create())
.scope((r) => r.record.ownedDHTRecordPointer);
final contactInvitationRecords =
await (await DHTShortArray.create(parent: accountRec.key))
.scope((r) => r.record.ownedDHTRecordPointer);
// Make account object
final account = proto.Account()
..profile = (proto.Profile()
..name = name
..title = title)
..contactList = contactList.toProto()
..contactInvitationRecords = contactInvitationRecords.toProto();
// Write account key

View File

@ -1700,6 +1700,7 @@ class ContactInvitationRecord extends $pb.GeneratedMessage {
..aOM<TypedKey>(4, _omitFieldNames ? '' : 'chatRecordKey', subBuilder: TypedKey.create)
..a<$fixnum.Int64>(5, _omitFieldNames ? '' : 'expiration', $pb.PbFieldType.OU6, defaultOrMaker: $fixnum.Int64.ZERO)
..a<$core.List<$core.int>>(6, _omitFieldNames ? '' : 'invitation', $pb.PbFieldType.OY)
..aOS(7, _omitFieldNames ? '' : 'message')
..hasRequiredFields = false
;
@ -1785,6 +1786,15 @@ class ContactInvitationRecord extends $pb.GeneratedMessage {
$core.bool hasInvitation() => $_has(5);
@$pb.TagNumber(6)
void clearInvitation() => clearField(6);
@$pb.TagNumber(7)
$core.String get message => $_getSZ(6);
@$pb.TagNumber(7)
set message($core.String v) { $_setString(6, v); }
@$pb.TagNumber(7)
$core.bool hasMessage() => $_has(6);
@$pb.TagNumber(7)
void clearMessage() => clearField(7);
}

View File

@ -463,6 +463,7 @@ const ContactInvitationRecord$json = {
{'1': 'chat_record_key', '3': 4, '4': 1, '5': 11, '6': '.TypedKey', '10': 'chatRecordKey'},
{'1': 'expiration', '3': 5, '4': 1, '5': 4, '10': 'expiration'},
{'1': 'invitation', '3': 6, '4': 1, '5': 12, '10': 'invitation'},
{'1': 'message', '3': 7, '4': 1, '5': 9, '10': 'message'},
],
};
@ -473,5 +474,5 @@ final $typed_data.Uint8List contactInvitationRecordDescriptor = $convert.base64D
'GAIgASgLMgouQ3J5cHRvS2V5Ugl3cml0ZXJLZXkSLwoNd3JpdGVyX3NlY3JldBgDIAEoCzIKLk'
'NyeXB0b0tleVIMd3JpdGVyU2VjcmV0EjEKD2NoYXRfcmVjb3JkX2tleRgEIAEoCzIJLlR5cGVk'
'S2V5Ug1jaGF0UmVjb3JkS2V5Eh4KCmV4cGlyYXRpb24YBSABKARSCmV4cGlyYXRpb24SHgoKaW'
'52aXRhdGlvbhgGIAEoDFIKaW52aXRhdGlvbg==');
'52aXRhdGlvbhgGIAEoDFIKaW52aXRhdGlvbhIYCgdtZXNzYWdlGAcgASgJUgdtZXNzYWdl');

View File

@ -344,6 +344,8 @@ message ContactInvitationRecord {
TypedKey chat_record_key = 4;
// Expiration timestamp
uint64 expiration = 5;
// A copy of the raw SignedContactResponse invitation bytes post-encryption and signing
// A copy of the raw SignedContactInvitation invitation bytes post-encryption and signing
bytes invitation = 6;
// The message sent along with the invitation
string message = 7;
}

View File

@ -4,11 +4,13 @@ import 'package:flutter_animate/flutter_animate.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_translate/flutter_translate.dart';
import '../../components/contact_invitation_list_widget.dart';
import '../../components/contact_list_widget.dart';
import '../../components/profile.dart';
import '../../entities/local_account.dart';
import '../../entities/proto.dart' as proto;
import '../../providers/account.dart';
import '../../providers/contact.dart';
import '../../providers/local_accounts.dart';
import '../../providers/logins.dart';
import '../../tools/tools.dart';
@ -24,7 +26,6 @@ class AccountPage extends ConsumerStatefulWidget {
class AccountPageState extends ConsumerState<AccountPage> {
final _unfocusNode = FocusNode();
TypedKey? _selectedAccount;
List<proto.Contact> _contactList = List<proto.Contact>.empty(growable: true);
@override
void initState() {
@ -62,9 +63,18 @@ class AccountPageState extends ConsumerState<AccountPage> {
proto.Account account,
// ignore: prefer_expression_function_bodies
) {
final contactInvitationRecordList =
ref.watch(fetchContactInvitationRecordsProvider).asData?.value ??
const IListConst([]);
final contactList = ref.watch(fetchContactListProvider).asData?.value ??
const IListConst([]);
return Column(children: <Widget>[
ProfileWidget(name: account.profile.name, title: account.profile.title),
ContactListWidget(contactList: _contactList)
if (contactInvitationRecordList.isNotEmpty)
ContactInvitationListWidget(
contactInvitationRecordList: contactInvitationRecordList),
ContactListWidget(contactList: contactList),
]);
}

View File

@ -110,9 +110,11 @@ class NewAccountPageState extends ConsumerState<NewAccountPage> {
try {
await onSubmit(_formKey);
} finally {
setState(() {
isInAsyncCall = false;
});
if (mounted) {
setState(() {
isInAsyncCall = false;
});
}
}
}
},

View File

@ -1,17 +1,30 @@
import 'dart:typed_data';
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
import 'package:fixnum/fixnum.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import '../entities/local_account.dart';
import '../entities/proto.dart' as proto;
import '../entities/proto.dart'
show
Contact,
ContactInvitation,
ContactInvitationRecord,
ContactRequest,
ContactRequestPrivate,
SignedContactInvitation;
import '../tools/tools.dart';
import '../veilid_support/veilid_support.dart';
import 'account.dart';
part 'contact.g.dart';
Future<Uint8List> createContactInvitation(
{required ActiveAccountInfo activeAccountInfo,
required EncryptionKeyType encryptionKeyType,
required String encryptionKey,
required String message,
required Timestamp? expiration}) async {
final pool = await DHTRecordPool.instance();
final accountRecordKey =
@ -39,7 +52,7 @@ Future<Uint8List> createContactInvitation(
await (await pool.create(parent: accountRecordKey))
.deleteScope((localChatRecord) async {
// Make ContactRequestPrivate and encrypt with the writer secret
final crpriv = proto.ContactRequestPrivate()
final crpriv = ContactRequestPrivate()
..writerKey = writer.key.toProto()
..profile = activeAccountInfo.account.profile
..accountMasterRecordKey =
@ -51,7 +64,7 @@ Future<Uint8List> createContactInvitation(
await cs.encryptNoAuthWithNonce(crprivbytes, writer.secret);
// Create ContactRequest and embed contactrequestprivate
final creq = proto.ContactRequest()
final creq = ContactRequest()
..encryptionKeyType = encryptionKeyType.toProto()
..private = encryptedContactRequestPrivate;
@ -66,24 +79,25 @@ Future<Uint8List> createContactInvitation(
await inboxRecord.eventualWriteProtobuf(creq);
// Create ContactInvitation and SignedContactInvitation
final cinv = proto.ContactInvitation()
final cinv = ContactInvitation()
..contactRequestRecordKey = inboxRecord.key.toProto()
..writerSecret = encryptedSecret;
final cinvbytes = cinv.writeToBuffer();
final scinv = proto.SignedContactInvitation()
final scinv = SignedContactInvitation()
..contactInvitation = cinvbytes
..identitySignature =
(await cs.sign(identityKey, identitySecret, cinvbytes)).toProto();
signedContactInvitationBytes = scinv.writeToBuffer();
// Create ContactInvitationRecord
final cinvrec = proto.ContactInvitationRecord()
final cinvrec = ContactInvitationRecord()
..contactRequestRecordKey = inboxRecord.key.toProto()
..writerKey = writer.key.toProto()
..writerSecret = writer.secret.toProto()
..chatRecordKey = localChatRecord.key.toProto()
..expiration = expiration?.toInt64() ?? Int64.ZERO
..invitation = signedContactInvitationBytes;
..invitation = signedContactInvitationBytes
..message = message;
// Add ContactInvitationRecord to local table if possible
// if this fails, don't keep retrying, user can try again later
@ -101,3 +115,64 @@ Future<Uint8List> createContactInvitation(
return signedContactInvitationBytes;
}
/// Get the active account contact invitation list
@riverpod
Future<IList<ContactInvitationRecord>?> fetchContactInvitationRecords(
FetchContactInvitationRecordsRef ref) async {
// See if we've logged into this account or if it is locked
final activeAccountInfo = await ref.watch(fetchActiveAccountProvider.future);
if (activeAccountInfo == null) {
return null;
}
final accountRecordKey =
activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey;
// Decode the contact invitation list from the DHT
IList<ContactInvitationRecord> out = const IListConst([]);
await (await DHTShortArray.openOwned(
proto.OwnedDHTRecordPointerProto.fromProto(
activeAccountInfo.account.contactInvitationRecords),
parent: accountRecordKey))
.scope((cirList) async {
for (var i = 0; i < cirList.length; i++) {
final cir = await cirList.getItem(i);
if (cir == null) {
throw StateError('Failed to get contact invitation record');
}
out = out.add(ContactInvitationRecord.fromBuffer(cir));
}
});
return out;
}
/// Get the active account contact list
@riverpod
Future<IList<Contact>?> fetchContactList(FetchContactListRef ref) async {
// See if we've logged into this account or if it is locked
final activeAccountInfo = await ref.watch(fetchActiveAccountProvider.future);
if (activeAccountInfo == null) {
return null;
}
final accountRecordKey =
activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey;
// Decode the contact list from the DHT
IList<Contact> out = const IListConst([]);
await (await DHTShortArray.openOwned(
proto.OwnedDHTRecordPointerProto.fromProto(
activeAccountInfo.account.contactList),
parent: accountRecordKey))
.scope((cList) async {
for (var i = 0; i < cList.length; i++) {
final cir = await cList.getItem(i);
if (cir == null) {
throw StateError('Failed to get contact');
}
out = out.add(Contact.fromBuffer(cir));
}
});
return out;
}

View File

@ -0,0 +1,47 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'contact.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
String _$fetchContactInvitationRecordsHash() =>
r'fcedc1807c6cb25ac6c2c42b372ec04abd4b911f';
/// Get the active account contact invitation list
///
/// Copied from [fetchContactInvitationRecords].
@ProviderFor(fetchContactInvitationRecords)
final fetchContactInvitationRecordsProvider =
AutoDisposeFutureProvider<IList<ContactInvitationRecord>?>.internal(
fetchContactInvitationRecords,
name: r'fetchContactInvitationRecordsProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
? null
: _$fetchContactInvitationRecordsHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef FetchContactInvitationRecordsRef
= AutoDisposeFutureProviderRef<IList<ContactInvitationRecord>?>;
String _$fetchContactListHash() => r'60ae4f117fc51c0870449563aedca7baf51cc254';
/// Get the active account contact list
///
/// Copied from [fetchContactList].
@ProviderFor(fetchContactList)
final fetchContactListProvider =
AutoDisposeFutureProvider<IList<Contact>?>.internal(
fetchContactList,
name: r'fetchContactListProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
? null
: _$fetchContactListHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef FetchContactListRef = AutoDisposeFutureProviderRef<IList<Contact>?>;
// ignore_for_file: unnecessary_raw_strings, subtype_of_sealed_class, invalid_use_of_internal_member, do_not_use_environment, prefer_const_constructors, public_member_api_docs, avoid_private_typedef_functions

View File

@ -33,7 +33,7 @@ class WindowControl extends _$WindowControl {
const windowOptions = WindowOptions(
size: Size(768, 1024),
//minimumSize: Size(480, 640),
//minimumSize: Size(480, 480),
center: true,
backgroundColor: Colors.transparent,
skipTaskbar: false,

View File

@ -1,6 +1,7 @@
import 'package:blurry_modal_progress_hud/blurry_modal_progress_hud.dart';
import 'package:flutter/material.dart';
import 'package:flutter_spinkit/flutter_spinkit.dart';
import 'package:flutter_translate/flutter_translate.dart';
import 'package:motion_toast/motion_toast.dart';
import 'package:quickalert/quickalert.dart';
@ -54,7 +55,14 @@ Future<void> showErrorModal(
void showErrorToast(BuildContext context, String message) {
MotionToast.error(
title: Text("Error"),
title: Text(translate('toast.error')),
description: Text(message),
).show(context);
}
void showInfoToast(BuildContext context, String message) {
MotionToast.info(
title: Text(translate('toast.info')),
description: Text(message),
).show(context);
}

View File

@ -98,7 +98,7 @@ class DHTRecord {
return null;
}
final lastSeq = _subkeySeqCache[subkey];
if (lastSeq != null && valueData.seq <= lastSeq) {
if (onlyUpdates && lastSeq != null && valueData.seq <= lastSeq) {
return null;
}
final out = _crypto.decrypt(valueData.data, subkey);
@ -137,11 +137,17 @@ class DHTRecord {
newValue = await _crypto.encrypt(newValue, subkey);
// Set the new data if possible
final valueData = await _routingContext.setDHTValue(
var valueData = await _routingContext.setDHTValue(
_recordDescriptor.key, subkey, newValue);
if (valueData == null) {
// Get the data to check its sequence number
valueData = await _routingContext.getDHTValue(
_recordDescriptor.key, subkey, false);
assert(valueData != null, "can't get value that was just set");
_subkeySeqCache[subkey] = valueData!.seq;
return null;
}
_subkeySeqCache[subkey] = valueData.seq;
return valueData.data;
}
@ -157,6 +163,12 @@ class DHTRecord {
// Repeat if newer data on the network was found
} while (valueData != null);
// Get the data to check its sequence number
valueData =
await _routingContext.getDHTValue(_recordDescriptor.key, subkey, false);
assert(valueData != null, "can't get value that was just set");
_subkeySeqCache[subkey] = valueData!.seq;
}
Future<void> eventualUpdateBytes(
@ -186,6 +198,12 @@ class DHTRecord {
// Repeat if newer data on the network was found
} while (valueData != null);
// Get the data to check its sequence number
valueData =
await _routingContext.getDHTValue(_recordDescriptor.key, subkey, false);
assert(valueData != null, "can't get value that was just set");
_subkeySeqCache[subkey] = valueData!.seq;
}
Future<T?> tryWriteJson<T>(T Function(dynamic) fromJson, T newValue,

View File

@ -64,6 +64,9 @@ class DHTShortArray {
crypto: crypto);
try {
final dhtShortArray = DHTShortArray._(headRecord: dhtRecord);
if (!await dhtShortArray._tryWriteHead()) {
throw StateError('Failed to write head at this time');
}
return dhtShortArray;
} on Exception catch (_) {
await dhtRecord.delete();
@ -327,7 +330,7 @@ class DHTShortArray {
// xxx: free list optimization here?
}
int length() => _head.index.length;
int get length => _head.index.length;
Future<Uint8List?> getItem(int pos, {bool forceRefresh = false}) async {
await _refreshHead(forceRefresh: forceRefresh, onlyUpdates: true);

View File

@ -42,7 +42,7 @@ packages:
source: hosted
version: "2.0.1"
archive:
dependency: transitive
dependency: "direct main"
description:
name: archive
sha256: "0c8368c9b3f0abbc193b9d6133649a614204b528982bebc7026372d61677ce3a"
@ -81,6 +81,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "3.1.1"
basic_utils:
dependency: "direct main"
description:
name: basic_utils
sha256: "1fb8c5493fc1b9500512b2e153c0b9bcc9e281621cde7f810420f4761be9e38d"
url: "https://pub.dev"
source: hosted
version: "5.6.1"
blurry_modal_progress_hud:
dependency: "direct main"
description:
@ -499,6 +507,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.3.6"
flutter_slidable:
dependency: "direct main"
description:
name: flutter_slidable
sha256: cc4231579e3eae41ae166660df717f4bad1359c87f4a4322ad8ba1befeb3d2be
url: "https://pub.dev"
source: hosted
version: "3.0.0"
flutter_spinkit:
dependency: "direct main"
description:
@ -925,6 +941,22 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.2.3"
qr:
dependency: transitive
description:
name: qr
sha256: "64957a3930367bf97cc211a5af99551d630f2f4625e38af10edd6b19131b64b3"
url: "https://pub.dev"
source: hosted
version: "3.0.1"
qr_flutter:
dependency: "direct main"
description:
name: qr_flutter
sha256: "5095f0fc6e3f71d08adef8feccc8cea4f12eec18a2e31c2e8d82cb6019f4b097"
url: "https://pub.dev"
source: hosted
version: "4.1.0"
quickalert:
dependency: "direct main"
description:
@ -1005,6 +1037,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "3.0.1"
searchable_listview:
dependency: "direct main"
description:
name: searchable_listview
sha256: "0a158665571e03890408e2d5569f0673a36f79fac5a671a54495b52f67b93b63"
url: "https://pub.dev"
source: hosted
version: "2.4.0"
share_plus:
dependency: "direct main"
description:

View File

@ -10,8 +10,10 @@ environment:
dependencies:
animated_theme_switcher: ^2.0.7
ansicolor: ^2.0.1
archive: ^3.3.7
awesome_extensions: ^2.0.9
badges: ^3.1.1
basic_utils: ^5.6.1
blurry_modal_progress_hud: ^1.1.0
change_case: ^1.1.0
charcode: ^1.3.1
@ -30,6 +32,7 @@ dependencies:
flutter_localizations:
sdk: flutter
flutter_riverpod: ^2.1.3
flutter_slidable: ^3.0.0
flutter_spinkit: ^5.2.0
flutter_svg: ^2.0.7
flutter_translate: ^4.0.4
@ -45,10 +48,12 @@ dependencies:
path_provider: ^2.0.11
pinput: ^2.3.0
protobuf: ^3.0.0
qr_flutter: ^4.1.0
quickalert: ^1.0.1
radix_colors: ^1.0.4
reorderable_grid: ^1.0.7
riverpod_annotation: ^2.1.1
searchable_listview: ^2.4.0
share_plus: ^7.0.2
shared_preferences: ^2.0.15
signal_strength_indicator: ^0.4.1