accept via paste

This commit is contained in:
Christien Rioux 2023-08-05 01:00:46 -04:00
parent 7496a1a2a7
commit 8bb8285e50
27 changed files with 829 additions and 324 deletions

Binary file not shown.

View File

@ -27,7 +27,10 @@
},
"button": {
"ok": "Ok",
"cancel": "Cancel"
"cancel": "Cancel",
"delete": "Delete",
"accept": "Accept",
"reject": "Reject"
},
"toast": {
"error": "Error",
@ -48,15 +51,17 @@
"missing_account_title": "Missing Account",
"missing_account_text": "Account is missing, removing from list",
"invalid_account_title": "Invalid Account",
"invalid_account_text": "Account is invalid, removing from list"
"invalid_account_text": "Account is invalid, removing from list",
"contact_invitations": "Contact Invitations"
},
"empty_contact_list": {
"invite_people": "Invite people to VeilidChat"
},
"accounts_menu": {
"invite_contact": "Invite Contact",
"send_invite": "Send Invite",
"receive_invite": "Receive Invite"
"create_invite": "Create Invite",
"scan_invite": "Scan Invite",
"paste_invite": "Paste Invite"
},
"send_invite_dialog": {
"connect_with_me": "Connect with me on VeilidChat!",
@ -78,6 +83,11 @@
"copy_invitation": "Copy Invitation",
"invitation_copied": "Invitation Copied"
},
"paste_invite_dialog": {
"paste_invite_here": "Paste your contact invite here:",
"paste": "Paste",
"message_from_contact": "Message from contact"
},
"enter_pin_dialog": {
"enter_pin": "Enter PIN",
"reenter_pin": "Re-Enter PIN To Confirm"

View File

@ -81,52 +81,48 @@ class ChatComponentState extends ConsumerState<ChatComponent> {
return DefaultTextStyle(
style: textTheme.bodySmall!,
child: Align(
alignment: AlignmentDirectional.centerEnd,
child: Container(
decoration: BoxDecoration(
color: scale.primaryScale.appBackground,
),
child: Stack(
alignment: AlignmentDirectional.centerEnd,
child: Stack(
children: [
Column(
children: [
Column(
children: [
Container(
height: 48,
decoration: BoxDecoration(
color: scale.primaryScale.subtleBackground,
),
child: Align(
alignment: AlignmentDirectional.centerStart,
child: Padding(
padding: const EdgeInsetsDirectional.fromSTEB(
16, 0, 16, 0),
child: Text("current contact",
textAlign: TextAlign.start,
style: textTheme.titleMedium),
),
),
Container(
height: 48,
decoration: BoxDecoration(
color: scale.primaryScale.subtleBackground,
),
child: Align(
alignment: AlignmentDirectional.centerStart,
child: Padding(
padding:
const EdgeInsetsDirectional.fromSTEB(16, 0, 16, 0),
child: Text("current contact",
textAlign: TextAlign.start,
style: textTheme.titleMedium),
),
Expanded(
child: Container(
decoration: const BoxDecoration(),
child: Chat(
theme: chatTheme,
messages: _messages,
//onAttachmentPressed: _handleAttachmentPressed,
//onMessageTap: _handleMessageTap,
//onPreviewDataFetched: _handlePreviewDataFetched,
),
),
Expanded(
child: Container(
decoration: const BoxDecoration(),
child: Chat(
theme: chatTheme,
messages: _messages,
//onAttachmentPressed: _handleAttachmentPressed,
//onMessageTap: _handleMessageTap,
//onPreviewDataFetched: _handlePreviewDataFetched,
onSendPressed: _handleSendPressed,
showUserAvatars: true,
showUserNames: true,
user: _user,
),
),
onSendPressed: _handleSendPressed,
showUserAvatars: true,
showUserNames: true,
user: _user,
),
],
),
),
],
),
)));
],
),
));
}
}

View File

@ -23,7 +23,7 @@ class ContactInvitationDisplayDialog extends ConsumerStatefulWidget {
final String name;
final String message;
final Future<Uint8List> generator;
final FutureOr<Uint8List> generator;
@override
ContactInvitationDisplayDialogState createState() =>
@ -35,7 +35,7 @@ class ContactInvitationDisplayDialog extends ConsumerStatefulWidget {
properties
..add(StringProperty('name', name))
..add(StringProperty('message', message))
..add(DiagnosticsProperty<Future<Uint8List>?>('generator', generator));
..add(DiagnosticsProperty<FutureOr<Uint8List>?>('generator', generator));
}
}
@ -65,9 +65,9 @@ class ContactInvitationDisplayDialogState
repeat: true);
final msg = message.isNotEmpty ? '$message\n' : '';
return '$msg'
'--- BEGIN VEILIDCHAT CONTACT INVITE ---\n'
'--- BEGIN VEILIDCHAT CONTACT INVITE ----\n'
'$invite\n'
'--- END VEILIDCHAT CONTACT INVITE ---\n';
'---- END VEILIDCHAT CONTACT INVITE -----\n';
}
@override
@ -92,8 +92,6 @@ class ContactInvitationDisplayDialogState
Navigator.of(context).pop();
return const Text('');
}
final compressedData =
Uint8List.fromList(BZip2Encoder().encode(data));
return Form(
key: formKey,
child: Column(
@ -113,7 +111,7 @@ class ContactInvitationDisplayDialogState
child: QrImageView.withQr(
size: 300,
qr: QrCode.fromUint8List(
data: compressedData,
data: data,
errorCorrectLevel:
QrErrorCorrectLevel.L))),
FittedBox(
@ -132,8 +130,8 @@ class ContactInvitationDisplayDialogState
translate(
'send_invite_dialog.invitation_copied'));
await Clipboard.setData(ClipboardData(
text: makeTextInvite(
widget.message, compressedData)));
text:
makeTextInvite(widget.message, data)));
},
),
]));

View File

@ -1,9 +1,14 @@
import 'package:flutter/foundation.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:veilid/veilid.dart';
import '../../entities/proto.dart' as proto;
import '../providers/account.dart';
import '../providers/contact.dart';
import '../tools/tools.dart';
import 'contact_invitation_display.dart';
class ContactInvitationItemWidget extends ConsumerWidget {
const ContactInvitationItemWidget(
@ -11,69 +16,110 @@ class ContactInvitationItemWidget extends ConsumerWidget {
final proto.ContactInvitationRecord contactInvitationRecord;
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(DiagnosticsProperty<proto.ContactInvitationRecord>(
'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(),
final theme = Theme.of(context);
final textTheme = theme.textTheme;
final scale = theme.extension<ScaleScheme>()!;
// A pane can dismiss the Slidable.
//dismissible: DismissiblePane(onDismissed: () {}),
return Container(
margin: const EdgeInsets.fromLTRB(4, 4, 4, 0),
clipBehavior: Clip.antiAlias,
decoration: ShapeDecoration(
color: scale.tertiaryScale.subtleBackground,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
)),
child: Slidable(
// Specify a key if the Slidable is dismissible.
key: ObjectKey(contactInvitationRecord),
endActionPane: ActionPane(
// A motion is a widget used to control how the pane animates.
motion: const DrawerMotion(),
// 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',
// 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) async {
final activeAccountInfo =
await ref.read(fetchActiveAccountProvider.future);
if (activeAccountInfo != null) {
await deleteContactInvitation(
activeAccountInfo: activeAccountInfo,
contactInvitationRecord: contactInvitationRecord);
ref.invalidate(fetchContactInvitationRecordsProvider);
}
},
backgroundColor: scale.tertiaryScale.background,
foregroundColor: scale.tertiaryScale.text,
icon: Icons.delete,
label: translate('button.delete'),
padding: const EdgeInsets.all(2)),
],
),
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',
// ),
// ],
// ),
// startActionPane: 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)));
// The child of the Slidable is what the user sees when the
// component is not dragged.
child: ListTile(
//title: Text(translate('contact_list.invitation')),
onTap: () async {
final activeAccountInfo =
await ref.read(fetchActiveAccountProvider.future);
if (activeAccountInfo != null) {
// ignore: use_build_context_synchronously
if (!context.mounted) {
return;
}
await showDialog<void>(
context: context,
builder: (context) => ContactInvitationDisplayDialog(
name: activeAccountInfo.localAccount.name,
message: contactInvitationRecord.message,
generator: Uint8List.fromList(
contactInvitationRecord.invitation),
));
}
},
subtitle: Text(contactInvitationRecord.message.isEmpty
? translate('contact_list.invitation')
: contactInvitationRecord.message),
iconColor: scale.tertiaryScale.background,
textColor: scale.tertiaryScale.text,
//Text(Timestamp.fromInt64(contactInvitationRecord.expiration) / ),
leading: const Icon(Icons.person_add))));
}
}

View File

@ -1,54 +1,75 @@
import 'package:awesome_extensions/awesome_extensions.dart';
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});
class ContactInvitationListWidget extends ConsumerStatefulWidget {
ContactInvitationListWidget({
required this.contactInvitationRecordList,
super.key,
});
final IList<proto.ContactInvitationRecord> contactInvitationRecordList;
@override
ContactInvitationListWidgetState createState() =>
ContactInvitationListWidgetState();
}
class ContactInvitationListWidgetState
extends ConsumerState<ContactInvitationListWidget> {
final ScrollController _scrollController = ScrollController();
@override
// ignore: prefer_expression_function_bodies
Widget build(BuildContext context, WidgetRef ref) {
Widget build(BuildContext context) {
final theme = Theme.of(context);
final textTheme = theme.textTheme;
//final scale = theme.extension<ScaleScheme>()!;
//final textTheme = theme.textTheme;
final scale = theme.extension<ScaleScheme>()!;
return Container(
decoration: BoxDecoration(
color: Theme.of(context).scaffoldBackgroundColor,
),
width: double.infinity,
constraints: const BoxConstraints(minHeight: 64, maxHeight: 200),
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;
})
Container(
width: double.infinity,
decoration: ShapeDecoration(
color: scale.grayScale.appBackground,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
)),
child: ListView.builder(
controller: _scrollController,
itemCount: widget.contactInvitationRecordList.length,
itemBuilder: (context, index) {
if (index < 0 ||
index >= widget.contactInvitationRecordList.length) {
return null;
}
return ContactInvitationItemWidget(
contactInvitationRecord:
widget.contactInvitationRecordList[index],
key: ObjectKey(
widget.contactInvitationRecordList[index]))
.paddingAll(2);
},
findChildIndexCallback: (key) {
final index = widget.contactInvitationRecordList.indexOf(
(key as ObjectKey).value!
as proto.ContactInvitationRecord);
if (index == -1) {
return null;
}
return index;
},
shrinkWrap: true,
)).paddingLTRB(8, 0, 8, 8).flexible()
],
),
);

View File

@ -1,7 +1,7 @@
import 'package:awesome_extensions/awesome_extensions.dart';
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';
@ -19,48 +19,55 @@ class ContactListWidget extends ConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) {
final theme = Theme.of(context);
final textTheme = theme.textTheme;
//final scale = theme.extension<ScaleScheme>()!;
final scale = theme.extension<ScaleScheme>()!;
return Container(
decoration: BoxDecoration(
color: Theme.of(context).scaffoldBackgroundColor,
width: double.infinity,
constraints: const BoxConstraints(
minHeight: 64,
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'Contacts',
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,
child: Column(children: [
Text(
'Contacts',
style: textTheme.bodyLarge,
).paddingAll(8),
Container(
width: double.infinity,
decoration: ShapeDecoration(
color: scale.grayScale.appBackground,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
)),
child: (contactList.isEmpty)
? const EmptyContactListWidget().toCenter()
: 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();
},
inputDecoration: InputDecoration(
labelText: translate('contact_list.search'),
fillColor: Colors.white,
focusedBorder: OutlineInputBorder(
borderSide: const BorderSide(
color: Colors.blue,
),
borderRadius: BorderRadius.circular(10),
),
),
),
borderRadius: BorderRadius.circular(10),
),
),
),
],
),
);
).expanded()
]),
).paddingLTRB(8, 0, 8, 65);
}
}

View File

@ -2,33 +2,33 @@ import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_translate/flutter_translate.dart';
import '../tools/tools.dart';
class EmptyContactListWidget extends ConsumerWidget {
const EmptyContactListWidget({super.key});
@override
// ignore: prefer_expression_function_bodies
Widget build(BuildContext context, WidgetRef ref) {
//
return Container(
decoration: BoxDecoration(
color: Theme.of(context).scaffoldBackgroundColor,
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.person_add_sharp,
color: Theme.of(context).disabledColor,
size: 48,
final theme = Theme.of(context);
final textTheme = theme.textTheme;
final scale = theme.extension<ScaleScheme>()!;
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.person_add_sharp,
color: scale.primaryScale.border,
size: 48,
),
Text(
translate('empty_contact_list.invite_people'),
style: textTheme.bodyMedium?.copyWith(
color: scale.primaryScale.border,
),
Text(
translate('empty_contact_list.invite_people'),
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Theme.of(context).disabledColor,
),
),
],
),
),
],
);
}
}

View File

@ -0,0 +1,251 @@
import 'dart:async';
import 'dart:typed_data';
import 'package:awesome_extensions/awesome_extensions.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_translate/flutter_translate.dart';
import 'package:quickalert/quickalert.dart';
import '../entities/local_account.dart';
import '../entities/proto.dart' as proto;
import '../providers/account.dart';
import '../providers/contact.dart';
import '../tools/tools.dart';
import '../veilid_support/veilid_support.dart';
import 'contact_invitation_display.dart';
import 'enter_pin.dart';
class PasteInviteDialog extends ConsumerStatefulWidget {
const PasteInviteDialog({super.key});
@override
PasteInviteDialogState createState() => PasteInviteDialogState();
}
class PasteInviteDialogState extends ConsumerState<PasteInviteDialog> {
final _pasteTextController = TextEditingController();
final _messageTextController = TextEditingController();
EncryptionKeyType _encryptionKeyType = EncryptionKeyType.none;
String _encryptionKey = '';
Timestamp? _expiration;
proto.SignedContactInvitation? _validInvitation;
@override
void initState() {
super.initState();
}
// Future<void> _onNoneEncryptionSelected(bool selected) async {
// setState(() {
// if (selected) {
// _encryptionKeyType = EncryptionKeyType.none;
// }
// });
// }
// Future<void> _onPinEncryptionSelected(bool selected) async {
// final description = translate('receive_invite_dialog.pin_description');
// final pin = await showDialog<String>(
// context: context,
// builder: (context) => EnterPinDialog(description: description));
// if (pin == null) {
// return;
// }
// // ignore: use_build_context_synchronously
// if (!context.mounted) {
// return;
// }
// final matchpin = await showDialog<String>(
// context: context,
// builder: (context) => EnterPinDialog(
// matchPin: pin,
// description: description,
// ));
// if (matchpin == null) {
// return;
// } else if (pin == matchpin) {
// setState(() {
// _encryptionKeyType = EncryptionKeyType.pin;
// _encryptionKey = pin;
// });
// } else {
// // ignore: use_build_context_synchronously
// if (!context.mounted) {
// return;
// }
// showErrorToast(
// context, translate('receive_invite_dialog.pin_does_not_match'));
// setState(() {
// _encryptionKeyType = EncryptionKeyType.none;
// _encryptionKey = '';
// });
// }
// }
// Future<void> _onPasswordEncryptionSelected(bool selected) async {
// setState(() {
// if (selected) {
// _encryptionKeyType = EncryptionKeyType.password;
// }
// });
// }
Future<void> _onPasteChanged(String text) async {
try {
final lines = text.split('\n');
if (lines.isEmpty) {
_validInvitation = null;
return;
}
var firstline =
lines.indexWhere((element) => element.contains('BEGIN VEILIDCHAT'));
firstline += 1;
var lastline =
lines.indexWhere((element) => element.contains('END VEILIDCHAT'));
if (lastline != -1) {
lastline = lines.length;
}
if (lastline <= firstline) {
_validInvitation = null;
return;
}
final inviteDataBase64 = lines.sublist(firstline, lastline).join();
final inviteData = base64UrlNoPadDecode(inviteDataBase64);
final signedContactInvitation =
proto.SignedContactInvitation.fromBuffer(inviteData);
final contactInvitationBytes =
Uint8List.fromList(signedContactInvitation.contactInvitation);
final contactInvitation =
proto.ContactInvitation.fromBuffer(contactInvitationBytes);
final contactRequestInboxKey = proto.TypedKeyProto.fromProto(
contactInvitation.contactRequestInboxKey);
// Open context request inbox subkey zero to get the contact request object
final pool = await DHTRecordPool.instance();
await (await pool.openRead(contactRequestInboxKey))
.deleteScope((contactRequestInbox) async {
//
final contactRequest = await contactRequestInbox
.getProtobuf(proto.ContactRequest.fromBuffer);
// Decrypt contact request private
final encryptionKeyType =
EncryptionKeyType.fromProto(contactRequest!.encryptionKeyType);
late final SecretKey writerSecret;
switch (encryptionKeyType) {
case EncryptionKeyType.none:
writerSecret = SecretKey.fromBytes(
Uint8List.fromList(contactInvitation.writerSecret));
case EncryptionKeyType.pin:
//
case EncryptionKeyType.password:
//
}
final cs =
await pool.veilid.getCryptoSystem(contactRequestInboxKey.kind);
final contactRequestPrivateBytes = await cs.decryptNoAuthWithNonce(
Uint8List.fromList(contactRequest.private), writerSecret);
final contactRequestPrivate =
proto.ContactRequestPrivate.fromBuffer(contactRequestPrivateBytes);
final contactIdentityMasterRecordKey = proto.TypedKeyProto.fromProto(
contactRequestPrivate.identityMasterRecordKey);
// Fetch the account master
final contactIdentityMaster = await openIdentityMaster(
identityMasterRecordKey: contactIdentityMasterRecordKey);
// Verify
final signature = proto.SignatureProto.fromProto(
signedContactInvitation.identitySignature);
await cs.verify(contactIdentityMaster.identityPublicKey,
contactInvitationBytes, signature);
// Verify expiration
//xxx
_validInvitation = signedContactInvitation;
});
} on Exception catch (_) {
_validInvitation = null;
}
}
@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 height = MediaQuery.of(context).size.height;
return SizedBox(
height: 400,
child: SingleChildScrollView(
padding: const EdgeInsets.all(8),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Text(
translate('paste_invite_dialog.paste_invite_here'),
).paddingAll(8),
Container(
constraints: const BoxConstraints(maxHeight: 200),
child: TextField(
onChanged: _onPasteChanged,
style: textTheme.labelSmall!
.copyWith(fontFamily: 'Victor Mono', fontSize: 11),
keyboardType: TextInputType.multiline,
maxLines: null,
controller: _pasteTextController,
decoration: const InputDecoration(
border: OutlineInputBorder(),
hintText: '--- BEGIN VEILIDCHAT CONTACT INVITE ----\n'
'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX\n'
'---- END VEILIDCHAT CONTACT INVITE -----\n',
//labelText: translate('paste_invite_dialog.paste')
),
).paddingAll(8)),
if (_validInvitation != null)
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
ElevatedButton.icon(
icon: const Icon(Icons.check_circle),
label: Text(translate('button.accept')),
onPressed: () {
//
},
),
ElevatedButton.icon(
icon: const Icon(Icons.cancel),
label: Text(translate('button.reject')),
onPressed: () {
//
},
)
],
),
TextField(
enabled: false,
controller: _messageTextController,
style: Theme.of(context).textTheme.bodySmall,
).paddingAll(8),
],
),
),
);
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(DiagnosticsProperty<TextEditingController>(
'messageTextController', _messageTextController));
}
}

View File

@ -1,7 +1,10 @@
import 'package:awesome_extensions/awesome_extensions.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../tools/tools.dart';
class ProfileWidget extends ConsumerWidget {
const ProfileWidget({
required this.name,
@ -15,16 +18,24 @@ class ProfileWidget extends ConsumerWidget {
@override
// ignore: prefer_expression_function_bodies
Widget build(BuildContext context, WidgetRef ref) {
// final logins = ref.watch(loginsProvider);
final theme = Theme.of(context);
final scale = theme.extension<ScaleScheme>()!;
final textTheme = theme.textTheme;
return ConstrainedBox(
constraints: const BoxConstraints(maxHeight: 300),
child: Column(children: [
Text('Profile', style: Theme.of(context).textTheme.headlineMedium),
Text(name, style: Theme.of(context).textTheme.bodyMedium),
return Container(
width: double.infinity,
decoration: ShapeDecoration(
color: scale.primaryScale.subtleBackground,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
side: BorderSide(color: scale.primaryScale.border))),
child: Column(mainAxisSize: MainAxisSize.min, children: [
Text(name, style: Theme.of(context).textTheme.headlineSmall)
.paddingAll(8),
if (title != null && title!.isNotEmpty)
Text(title!, style: Theme.of(context).textTheme.bodySmall),
]));
Text(title!, style: Theme.of(context).textTheme.bodyMedium)
.paddingLTRB(8, 0, 8, 8),
])).paddingAll(8);
}
@override

View File

@ -4,6 +4,7 @@ import 'dart:typed_data';
import 'package:awesome_extensions/awesome_extensions.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_translate/flutter_translate.dart';
import 'package:quickalert/quickalert.dart';
@ -143,6 +144,9 @@ class SendInviteDialogState extends ConsumerState<SendInviteDialog> {
).paddingAll(8),
TextField(
controller: _messageTextController,
inputFormatters: [
LengthLimitingTextInputFormatter(256),
],
decoration: InputDecoration(
border: const OutlineInputBorder(),
hintText: translate('send_invite_dialog.enter_message_hint'),

View File

@ -999,8 +999,8 @@ class Contact extends $pb.GeneratedMessage {
..aOM<Profile>(1, _omitFieldNames ? '' : 'editedProfile', subBuilder: Profile.create)
..aOM<Profile>(2, _omitFieldNames ? '' : 'remoteProfile', subBuilder: Profile.create)
..aOS(3, _omitFieldNames ? '' : 'remoteIdentity')
..aOM<TypedKey>(4, _omitFieldNames ? '' : 'remoteConversation', subBuilder: TypedKey.create)
..aOM<TypedKey>(5, _omitFieldNames ? '' : 'localConversation', subBuilder: TypedKey.create)
..aOM<TypedKey>(4, _omitFieldNames ? '' : 'remoteConversationKey', subBuilder: TypedKey.create)
..aOM<OwnedDHTRecordPointer>(5, _omitFieldNames ? '' : 'localConversation', subBuilder: OwnedDHTRecordPointer.create)
..aOB(6, _omitFieldNames ? '' : 'showAvailability')
..hasRequiredFields = false
;
@ -1058,26 +1058,26 @@ class Contact extends $pb.GeneratedMessage {
void clearRemoteIdentity() => clearField(3);
@$pb.TagNumber(4)
TypedKey get remoteConversation => $_getN(3);
TypedKey get remoteConversationKey => $_getN(3);
@$pb.TagNumber(4)
set remoteConversation(TypedKey v) { setField(4, v); }
set remoteConversationKey(TypedKey v) { setField(4, v); }
@$pb.TagNumber(4)
$core.bool hasRemoteConversation() => $_has(3);
$core.bool hasRemoteConversationKey() => $_has(3);
@$pb.TagNumber(4)
void clearRemoteConversation() => clearField(4);
void clearRemoteConversationKey() => clearField(4);
@$pb.TagNumber(4)
TypedKey ensureRemoteConversation() => $_ensure(3);
TypedKey ensureRemoteConversationKey() => $_ensure(3);
@$pb.TagNumber(5)
TypedKey get localConversation => $_getN(4);
OwnedDHTRecordPointer get localConversation => $_getN(4);
@$pb.TagNumber(5)
set localConversation(TypedKey v) { setField(5, v); }
set localConversation(OwnedDHTRecordPointer v) { setField(5, v); }
@$pb.TagNumber(5)
$core.bool hasLocalConversation() => $_has(4);
@$pb.TagNumber(5)
void clearLocalConversation() => clearField(5);
@$pb.TagNumber(5)
TypedKey ensureLocalConversation() => $_ensure(4);
OwnedDHTRecordPointer ensureLocalConversation() => $_ensure(4);
@$pb.TagNumber(6)
$core.bool get showAvailability => $_getBF(5);
@ -1324,7 +1324,7 @@ class ContactInvitation extends $pb.GeneratedMessage {
factory ContactInvitation.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r);
static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'ContactInvitation', createEmptyInstance: create)
..aOM<TypedKey>(1, _omitFieldNames ? '' : 'contactRequestRecordKey', subBuilder: TypedKey.create)
..aOM<TypedKey>(1, _omitFieldNames ? '' : 'contactRequestInboxKey', subBuilder: TypedKey.create)
..a<$core.List<$core.int>>(2, _omitFieldNames ? '' : 'writerSecret', $pb.PbFieldType.OY)
..hasRequiredFields = false
;
@ -1351,15 +1351,15 @@ class ContactInvitation extends $pb.GeneratedMessage {
static ContactInvitation? _defaultInstance;
@$pb.TagNumber(1)
TypedKey get contactRequestRecordKey => $_getN(0);
TypedKey get contactRequestInboxKey => $_getN(0);
@$pb.TagNumber(1)
set contactRequestRecordKey(TypedKey v) { setField(1, v); }
set contactRequestInboxKey(TypedKey v) { setField(1, v); }
@$pb.TagNumber(1)
$core.bool hasContactRequestRecordKey() => $_has(0);
$core.bool hasContactRequestInboxKey() => $_has(0);
@$pb.TagNumber(1)
void clearContactRequestRecordKey() => clearField(1);
void clearContactRequestInboxKey() => clearField(1);
@$pb.TagNumber(1)
TypedKey ensureContactRequestRecordKey() => $_ensure(0);
TypedKey ensureContactRequestInboxKey() => $_ensure(0);
@$pb.TagNumber(2)
$core.List<$core.int> get writerSecret => $_getN(1);
@ -1486,7 +1486,7 @@ class ContactRequestPrivate extends $pb.GeneratedMessage {
static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'ContactRequestPrivate', createEmptyInstance: create)
..aOM<CryptoKey>(1, _omitFieldNames ? '' : 'writerKey', subBuilder: CryptoKey.create)
..aOM<Profile>(2, _omitFieldNames ? '' : 'profile', subBuilder: Profile.create)
..aOM<TypedKey>(3, _omitFieldNames ? '' : 'accountMasterRecordKey', subBuilder: TypedKey.create)
..aOM<TypedKey>(3, _omitFieldNames ? '' : 'identityMasterRecordKey', subBuilder: TypedKey.create)
..aOM<TypedKey>(4, _omitFieldNames ? '' : 'chatRecordKey', subBuilder: TypedKey.create)
..a<$fixnum.Int64>(5, _omitFieldNames ? '' : 'expiration', $pb.PbFieldType.OU6, defaultOrMaker: $fixnum.Int64.ZERO)
..hasRequiredFields = false
@ -1536,15 +1536,15 @@ class ContactRequestPrivate extends $pb.GeneratedMessage {
Profile ensureProfile() => $_ensure(1);
@$pb.TagNumber(3)
TypedKey get accountMasterRecordKey => $_getN(2);
TypedKey get identityMasterRecordKey => $_getN(2);
@$pb.TagNumber(3)
set accountMasterRecordKey(TypedKey v) { setField(3, v); }
set identityMasterRecordKey(TypedKey v) { setField(3, v); }
@$pb.TagNumber(3)
$core.bool hasAccountMasterRecordKey() => $_has(2);
$core.bool hasIdentityMasterRecordKey() => $_has(2);
@$pb.TagNumber(3)
void clearAccountMasterRecordKey() => clearField(3);
void clearIdentityMasterRecordKey() => clearField(3);
@$pb.TagNumber(3)
TypedKey ensureAccountMasterRecordKey() => $_ensure(2);
TypedKey ensureIdentityMasterRecordKey() => $_ensure(2);
@$pb.TagNumber(4)
TypedKey get chatRecordKey => $_getN(3);
@ -1576,7 +1576,7 @@ class ContactResponse extends $pb.GeneratedMessage {
static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'ContactResponse', createEmptyInstance: create)
..aOB(1, _omitFieldNames ? '' : 'accept')
..aOM<TypedKey>(2, _omitFieldNames ? '' : 'accountMasterRecordKey', subBuilder: TypedKey.create)
..aOM<TypedKey>(3, _omitFieldNames ? '' : 'chatRecordKey', subBuilder: TypedKey.create)
..aOM<TypedKey>(3, _omitFieldNames ? '' : 'remoteConversationKey', subBuilder: TypedKey.create)
..hasRequiredFields = false
;
@ -1622,15 +1622,15 @@ class ContactResponse extends $pb.GeneratedMessage {
TypedKey ensureAccountMasterRecordKey() => $_ensure(1);
@$pb.TagNumber(3)
TypedKey get chatRecordKey => $_getN(2);
TypedKey get remoteConversationKey => $_getN(2);
@$pb.TagNumber(3)
set chatRecordKey(TypedKey v) { setField(3, v); }
set remoteConversationKey(TypedKey v) { setField(3, v); }
@$pb.TagNumber(3)
$core.bool hasChatRecordKey() => $_has(2);
$core.bool hasRemoteConversationKey() => $_has(2);
@$pb.TagNumber(3)
void clearChatRecordKey() => clearField(3);
void clearRemoteConversationKey() => clearField(3);
@$pb.TagNumber(3)
TypedKey ensureChatRecordKey() => $_ensure(2);
TypedKey ensureRemoteConversationKey() => $_ensure(2);
}
class SignedContactResponse extends $pb.GeneratedMessage {
@ -1694,10 +1694,10 @@ class ContactInvitationRecord extends $pb.GeneratedMessage {
factory ContactInvitationRecord.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r);
static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'ContactInvitationRecord', createEmptyInstance: create)
..aOM<TypedKey>(1, _omitFieldNames ? '' : 'contactRequestRecordKey', subBuilder: TypedKey.create)
..aOM<OwnedDHTRecordPointer>(1, _omitFieldNames ? '' : 'contactRequestInbox', subBuilder: OwnedDHTRecordPointer.create)
..aOM<CryptoKey>(2, _omitFieldNames ? '' : 'writerKey', subBuilder: CryptoKey.create)
..aOM<CryptoKey>(3, _omitFieldNames ? '' : 'writerSecret', subBuilder: CryptoKey.create)
..aOM<TypedKey>(4, _omitFieldNames ? '' : 'chatRecordKey', subBuilder: TypedKey.create)
..aOM<OwnedDHTRecordPointer>(4, _omitFieldNames ? '' : 'localConversation', subBuilder: OwnedDHTRecordPointer.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')
@ -1726,15 +1726,15 @@ class ContactInvitationRecord extends $pb.GeneratedMessage {
static ContactInvitationRecord? _defaultInstance;
@$pb.TagNumber(1)
TypedKey get contactRequestRecordKey => $_getN(0);
OwnedDHTRecordPointer get contactRequestInbox => $_getN(0);
@$pb.TagNumber(1)
set contactRequestRecordKey(TypedKey v) { setField(1, v); }
set contactRequestInbox(OwnedDHTRecordPointer v) { setField(1, v); }
@$pb.TagNumber(1)
$core.bool hasContactRequestRecordKey() => $_has(0);
$core.bool hasContactRequestInbox() => $_has(0);
@$pb.TagNumber(1)
void clearContactRequestRecordKey() => clearField(1);
void clearContactRequestInbox() => clearField(1);
@$pb.TagNumber(1)
TypedKey ensureContactRequestRecordKey() => $_ensure(0);
OwnedDHTRecordPointer ensureContactRequestInbox() => $_ensure(0);
@$pb.TagNumber(2)
CryptoKey get writerKey => $_getN(1);
@ -1759,15 +1759,15 @@ class ContactInvitationRecord extends $pb.GeneratedMessage {
CryptoKey ensureWriterSecret() => $_ensure(2);
@$pb.TagNumber(4)
TypedKey get chatRecordKey => $_getN(3);
OwnedDHTRecordPointer get localConversation => $_getN(3);
@$pb.TagNumber(4)
set chatRecordKey(TypedKey v) { setField(4, v); }
set localConversation(OwnedDHTRecordPointer v) { setField(4, v); }
@$pb.TagNumber(4)
$core.bool hasChatRecordKey() => $_has(3);
$core.bool hasLocalConversation() => $_has(3);
@$pb.TagNumber(4)
void clearChatRecordKey() => clearField(4);
void clearLocalConversation() => clearField(4);
@$pb.TagNumber(4)
TypedKey ensureChatRecordKey() => $_ensure(3);
OwnedDHTRecordPointer ensureLocalConversation() => $_ensure(3);
@$pb.TagNumber(5)
$fixnum.Int64 get expiration => $_getI64(4);

View File

@ -287,8 +287,8 @@ const Contact$json = {
{'1': 'edited_profile', '3': 1, '4': 1, '5': 11, '6': '.Profile', '10': 'editedProfile'},
{'1': 'remote_profile', '3': 2, '4': 1, '5': 11, '6': '.Profile', '10': 'remoteProfile'},
{'1': 'remote_identity', '3': 3, '4': 1, '5': 9, '10': 'remoteIdentity'},
{'1': 'remote_conversation', '3': 4, '4': 1, '5': 11, '6': '.TypedKey', '10': 'remoteConversation'},
{'1': 'local_conversation', '3': 5, '4': 1, '5': 11, '6': '.TypedKey', '10': 'localConversation'},
{'1': 'remote_conversation_key', '3': 4, '4': 1, '5': 11, '6': '.TypedKey', '10': 'remoteConversationKey'},
{'1': 'local_conversation', '3': 5, '4': 1, '5': 11, '6': '.OwnedDHTRecordPointer', '10': 'localConversation'},
{'1': 'show_availability', '3': 6, '4': 1, '5': 8, '10': 'showAvailability'},
],
};
@ -297,10 +297,10 @@ const Contact$json = {
final $typed_data.Uint8List contactDescriptor = $convert.base64Decode(
'CgdDb250YWN0Ei8KDmVkaXRlZF9wcm9maWxlGAEgASgLMgguUHJvZmlsZVINZWRpdGVkUHJvZm'
'lsZRIvCg5yZW1vdGVfcHJvZmlsZRgCIAEoCzIILlByb2ZpbGVSDXJlbW90ZVByb2ZpbGUSJwoP'
'cmVtb3RlX2lkZW50aXR5GAMgASgJUg5yZW1vdGVJZGVudGl0eRI6ChNyZW1vdGVfY29udmVyc2'
'F0aW9uGAQgASgLMgkuVHlwZWRLZXlSEnJlbW90ZUNvbnZlcnNhdGlvbhI4ChJsb2NhbF9jb252'
'ZXJzYXRpb24YBSABKAsyCS5UeXBlZEtleVIRbG9jYWxDb252ZXJzYXRpb24SKwoRc2hvd19hdm'
'FpbGFiaWxpdHkYBiABKAhSEHNob3dBdmFpbGFiaWxpdHk=');
'cmVtb3RlX2lkZW50aXR5GAMgASgJUg5yZW1vdGVJZGVudGl0eRJBChdyZW1vdGVfY29udmVyc2'
'F0aW9uX2tleRgEIAEoCzIJLlR5cGVkS2V5UhVyZW1vdGVDb252ZXJzYXRpb25LZXkSRQoSbG9j'
'YWxfY29udmVyc2F0aW9uGAUgASgLMhYuT3duZWRESFRSZWNvcmRQb2ludGVyUhFsb2NhbENvbn'
'ZlcnNhdGlvbhIrChFzaG93X2F2YWlsYWJpbGl0eRgGIAEoCFIQc2hvd0F2YWlsYWJpbGl0eQ==');
@$core.Deprecated('Use profileDescriptor instead')
const Profile$json = {
@ -362,16 +362,16 @@ final $typed_data.Uint8List accountDescriptor = $convert.base64Decode(
const ContactInvitation$json = {
'1': 'ContactInvitation',
'2': [
{'1': 'contact_request_record_key', '3': 1, '4': 1, '5': 11, '6': '.TypedKey', '10': 'contactRequestRecordKey'},
{'1': 'contact_request_inbox_key', '3': 1, '4': 1, '5': 11, '6': '.TypedKey', '10': 'contactRequestInboxKey'},
{'1': 'writer_secret', '3': 2, '4': 1, '5': 12, '10': 'writerSecret'},
],
};
/// Descriptor for `ContactInvitation`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List contactInvitationDescriptor = $convert.base64Decode(
'ChFDb250YWN0SW52aXRhdGlvbhJGChpjb250YWN0X3JlcXVlc3RfcmVjb3JkX2tleRgBIAEoCz'
'IJLlR5cGVkS2V5Uhdjb250YWN0UmVxdWVzdFJlY29yZEtleRIjCg13cml0ZXJfc2VjcmV0GAIg'
'ASgMUgx3cml0ZXJTZWNyZXQ=');
'ChFDb250YWN0SW52aXRhdGlvbhJEChljb250YWN0X3JlcXVlc3RfaW5ib3hfa2V5GAEgASgLMg'
'kuVHlwZWRLZXlSFmNvbnRhY3RSZXF1ZXN0SW5ib3hLZXkSIwoNd3JpdGVyX3NlY3JldBgCIAEo'
'DFIMd3JpdGVyU2VjcmV0');
@$core.Deprecated('Use signedContactInvitationDescriptor instead')
const SignedContactInvitation$json = {
@ -408,7 +408,7 @@ const ContactRequestPrivate$json = {
'2': [
{'1': 'writer_key', '3': 1, '4': 1, '5': 11, '6': '.CryptoKey', '10': 'writerKey'},
{'1': 'profile', '3': 2, '4': 1, '5': 11, '6': '.Profile', '10': 'profile'},
{'1': 'account_master_record_key', '3': 3, '4': 1, '5': 11, '6': '.TypedKey', '10': 'accountMasterRecordKey'},
{'1': 'identity_master_record_key', '3': 3, '4': 1, '5': 11, '6': '.TypedKey', '10': 'identityMasterRecordKey'},
{'1': 'chat_record_key', '3': 4, '4': 1, '5': 11, '6': '.TypedKey', '10': 'chatRecordKey'},
{'1': 'expiration', '3': 5, '4': 1, '5': 4, '10': 'expiration'},
],
@ -417,10 +417,10 @@ const ContactRequestPrivate$json = {
/// Descriptor for `ContactRequestPrivate`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List contactRequestPrivateDescriptor = $convert.base64Decode(
'ChVDb250YWN0UmVxdWVzdFByaXZhdGUSKQoKd3JpdGVyX2tleRgBIAEoCzIKLkNyeXB0b0tleV'
'IJd3JpdGVyS2V5EiIKB3Byb2ZpbGUYAiABKAsyCC5Qcm9maWxlUgdwcm9maWxlEkQKGWFjY291'
'bnRfbWFzdGVyX3JlY29yZF9rZXkYAyABKAsyCS5UeXBlZEtleVIWYWNjb3VudE1hc3RlclJlY2'
'9yZEtleRIxCg9jaGF0X3JlY29yZF9rZXkYBCABKAsyCS5UeXBlZEtleVINY2hhdFJlY29yZEtl'
'eRIeCgpleHBpcmF0aW9uGAUgASgEUgpleHBpcmF0aW9u');
'IJd3JpdGVyS2V5EiIKB3Byb2ZpbGUYAiABKAsyCC5Qcm9maWxlUgdwcm9maWxlEkYKGmlkZW50'
'aXR5X21hc3Rlcl9yZWNvcmRfa2V5GAMgASgLMgkuVHlwZWRLZXlSF2lkZW50aXR5TWFzdGVyUm'
'Vjb3JkS2V5EjEKD2NoYXRfcmVjb3JkX2tleRgEIAEoCzIJLlR5cGVkS2V5Ug1jaGF0UmVjb3Jk'
'S2V5Eh4KCmV4cGlyYXRpb24YBSABKARSCmV4cGlyYXRpb24=');
@$core.Deprecated('Use contactResponseDescriptor instead')
const ContactResponse$json = {
@ -428,7 +428,7 @@ const ContactResponse$json = {
'2': [
{'1': 'accept', '3': 1, '4': 1, '5': 8, '10': 'accept'},
{'1': 'account_master_record_key', '3': 2, '4': 1, '5': 11, '6': '.TypedKey', '10': 'accountMasterRecordKey'},
{'1': 'chat_record_key', '3': 3, '4': 1, '5': 11, '6': '.TypedKey', '10': 'chatRecordKey'},
{'1': 'remote_conversation_key', '3': 3, '4': 1, '5': 11, '6': '.TypedKey', '10': 'remoteConversationKey'},
],
};
@ -436,7 +436,8 @@ const ContactResponse$json = {
final $typed_data.Uint8List contactResponseDescriptor = $convert.base64Decode(
'Cg9Db250YWN0UmVzcG9uc2USFgoGYWNjZXB0GAEgASgIUgZhY2NlcHQSRAoZYWNjb3VudF9tYX'
'N0ZXJfcmVjb3JkX2tleRgCIAEoCzIJLlR5cGVkS2V5UhZhY2NvdW50TWFzdGVyUmVjb3JkS2V5'
'EjEKD2NoYXRfcmVjb3JkX2tleRgDIAEoCzIJLlR5cGVkS2V5Ug1jaGF0UmVjb3JkS2V5');
'EkEKF3JlbW90ZV9jb252ZXJzYXRpb25fa2V5GAMgASgLMgkuVHlwZWRLZXlSFXJlbW90ZUNvbn'
'ZlcnNhdGlvbktleQ==');
@$core.Deprecated('Use signedContactResponseDescriptor instead')
const SignedContactResponse$json = {
@ -457,10 +458,10 @@ final $typed_data.Uint8List signedContactResponseDescriptor = $convert.base64Dec
const ContactInvitationRecord$json = {
'1': 'ContactInvitationRecord',
'2': [
{'1': 'contact_request_record_key', '3': 1, '4': 1, '5': 11, '6': '.TypedKey', '10': 'contactRequestRecordKey'},
{'1': 'contact_request_inbox', '3': 1, '4': 1, '5': 11, '6': '.OwnedDHTRecordPointer', '10': 'contactRequestInbox'},
{'1': 'writer_key', '3': 2, '4': 1, '5': 11, '6': '.CryptoKey', '10': 'writerKey'},
{'1': 'writer_secret', '3': 3, '4': 1, '5': 11, '6': '.CryptoKey', '10': 'writerSecret'},
{'1': 'chat_record_key', '3': 4, '4': 1, '5': 11, '6': '.TypedKey', '10': 'chatRecordKey'},
{'1': 'local_conversation', '3': 4, '4': 1, '5': 11, '6': '.OwnedDHTRecordPointer', '10': 'localConversation'},
{'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'},
@ -469,10 +470,11 @@ const ContactInvitationRecord$json = {
/// Descriptor for `ContactInvitationRecord`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List contactInvitationRecordDescriptor = $convert.base64Decode(
'ChdDb250YWN0SW52aXRhdGlvblJlY29yZBJGChpjb250YWN0X3JlcXVlc3RfcmVjb3JkX2tleR'
'gBIAEoCzIJLlR5cGVkS2V5Uhdjb250YWN0UmVxdWVzdFJlY29yZEtleRIpCgp3cml0ZXJfa2V5'
'GAIgASgLMgouQ3J5cHRvS2V5Ugl3cml0ZXJLZXkSLwoNd3JpdGVyX3NlY3JldBgDIAEoCzIKLk'
'NyeXB0b0tleVIMd3JpdGVyU2VjcmV0EjEKD2NoYXRfcmVjb3JkX2tleRgEIAEoCzIJLlR5cGVk'
'S2V5Ug1jaGF0UmVjb3JkS2V5Eh4KCmV4cGlyYXRpb24YBSABKARSCmV4cGlyYXRpb24SHgoKaW'
'52aXRhdGlvbhgGIAEoDFIKaW52aXRhdGlvbhIYCgdtZXNzYWdlGAcgASgJUgdtZXNzYWdl');
'ChdDb250YWN0SW52aXRhdGlvblJlY29yZBJKChVjb250YWN0X3JlcXVlc3RfaW5ib3gYASABKA'
'syFi5Pd25lZERIVFJlY29yZFBvaW50ZXJSE2NvbnRhY3RSZXF1ZXN0SW5ib3gSKQoKd3JpdGVy'
'X2tleRgCIAEoCzIKLkNyeXB0b0tleVIJd3JpdGVyS2V5Ei8KDXdyaXRlcl9zZWNyZXQYAyABKA'
'syCi5DcnlwdG9LZXlSDHdyaXRlclNlY3JldBJFChJsb2NhbF9jb252ZXJzYXRpb24YBCABKAsy'
'Fi5Pd25lZERIVFJlY29yZFBvaW50ZXJSEWxvY2FsQ29udmVyc2F0aW9uEh4KCmV4cGlyYXRpb2'
'4YBSABKARSCmV4cGlyYXRpb24SHgoKaW52aXRhdGlvbhgGIAEoDFIKaW52aXRhdGlvbhIYCgdt'
'ZXNzYWdlGAcgASgJUgdtZXNzYWdl');

View File

@ -201,9 +201,9 @@ message Contact {
// Copy of friend's identity (JSON) from remote conversation
string remote_identity = 3;
// Remote conversation key to sync from friend
TypedKey remote_conversation = 4;
TypedKey remote_conversation_key = 4;
// Our conversation key for friend to sync
TypedKey local_conversation = 5;
OwnedDHTRecordPointer local_conversation = 5;
// Show availability
bool show_availability = 6;
}
@ -274,10 +274,11 @@ enum EncryptionKeyType {
// Invitation that is shared for VeilidChat contact connections
// serialized to QR code or data blob, not send over DHT, out of band.
// Writer secret is unique to this invitation
// Writer secret is unique to this invitation. Writer public key is in the ContactRequestPrivate
// in the ContactRequestInbox subkey 0 DHT key
message ContactInvitation {
// Contact request DHT record key
TypedKey contact_request_record_key = 1;
TypedKey contact_request_inbox_key = 1;
// Writer secret key bytes possibly encrypted with nonce appended
bytes writer_secret = 2;
}
@ -306,8 +307,8 @@ message ContactRequestPrivate {
CryptoKey writer_key = 1;
// Snapshot of profile
Profile profile = 2;
// Account master dht key
TypedKey account_master_record_key = 3;
// Identity master dht key
TypedKey identity_master_record_key = 3;
// Local chat DHT record key
TypedKey chat_record_key = 4;
// Expiration timestamp
@ -321,7 +322,7 @@ message ContactResponse {
// Account master record key
TypedKey account_master_record_key = 2;
// Local chat DHT record key if accepted
TypedKey chat_record_key = 3;
TypedKey remote_conversation_key = 3;
}
// Signature of response with identity
@ -335,13 +336,14 @@ message SignedContactResponse {
// Contact request record kept in Account DHTList to keep track of extant contact invitations
message ContactInvitationRecord {
// Contact request unicastinbox DHT record key
TypedKey contact_request_record_key = 1;
// Unencrypted writer key for this request
// Contact request unicastinbox DHT record key (parent is accountkey)
OwnedDHTRecordPointer contact_request_inbox = 1;
// Writer key sent to contact for the contact_request_inbox smpl inbox subkey
CryptoKey writer_key = 2;
CryptoKey writer_secret = 3;
// Local chat DHT record key
TypedKey chat_record_key = 4;
// Writer secret sent encrypted in the invitation
CryptoKey writer_secret = 3;
// Local chat DHT record key (parent is accountkey, will be moved to Contact if accepted)
OwnedDHTRecordPointer local_conversation = 4;
// Expiration timestamp
uint64 expiration = 5;
// A copy of the raw SignedContactInvitation invitation bytes post-encryption and signing

View File

@ -1,3 +1,4 @@
import 'package:awesome_extensions/awesome_extensions.dart';
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
import 'package:flutter/material.dart';
import 'package:flutter_animate/flutter_animate.dart';
@ -56,6 +57,7 @@ class AccountPageState extends ConsumerState<AccountPage> {
return Center(child: Text("unlock account"));
}
/// We have an active, unlocked, user login
Widget buildUserAccount(
BuildContext context,
IList<LocalAccount> localAccounts,
@ -72,9 +74,15 @@ class AccountPageState extends ConsumerState<AccountPage> {
return Column(children: <Widget>[
ProfileWidget(name: account.profile.name, title: account.profile.title),
if (contactInvitationRecordList.isNotEmpty)
ContactInvitationListWidget(
contactInvitationRecordList: contactInvitationRecordList),
ContactListWidget(contactList: contactList),
ExpansionTile(
title: Text(translate('account_page.contact_invitations')),
initiallyExpanded: true,
children: [
ContactInvitationListWidget(
contactInvitationRecordList: contactInvitationRecordList)
],
),
ContactListWidget(contactList: contactList).expanded(),
]);
}

View File

@ -12,6 +12,7 @@ import 'package:stylish_bottom_bar/stylish_bottom_bar.dart';
import '../../components/bottom_sheet_action_button.dart';
import '../../components/contact_invitation_display.dart';
import '../../components/paste_invite_dialog.dart';
import '../../components/send_invite_dialog.dart';
import '../../tools/tools.dart';
import 'account_page.dart';
@ -129,6 +130,26 @@ class MainPagerState extends ConsumerState<MainPager>
});
}
Future<void> pasteContactInvitationDialog(BuildContext context) async {
await showDialog<void>(
context: context,
// ignore: prefer_expression_function_bodies
builder: (context) {
return const AlertDialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(20)),
),
contentPadding: EdgeInsets.only(
top: 10,
),
title: Text(
'Paste Contact Invite',
style: TextStyle(fontSize: 24),
),
content: PasteInviteDialog());
});
}
Widget _newContactInvitationBottomSheetBuilder(
// ignore: prefer_expression_function_bodies
BuildContext context) {
@ -153,17 +174,27 @@ class MainPagerState extends ConsumerState<MainPager>
await sendContactInvitationDialog(context);
},
iconSize: 64,
icon: const Icon(Icons.output)),
Text(translate('accounts_menu.send_invite'))
icon: const Icon(Icons.contact_page)),
Text(translate('accounts_menu.create_invite'))
]),
Column(mainAxisAlignment: MainAxisAlignment.center, children: [
IconButton(
onPressed: () {
onPressed: () async {
Navigator.pop(context);
},
iconSize: 64,
icon: const Icon(Icons.input)),
Text(translate('accounts_menu.receive_invite'))
icon: const Icon(Icons.qr_code_scanner)),
Text(translate('accounts_menu.scan_invite'))
]),
Column(mainAxisAlignment: MainAxisAlignment.center, children: [
IconButton(
onPressed: () async {
Navigator.pop(context);
await pasteContactInvitationDialog(context);
},
iconSize: 64,
icon: const Icon(Icons.paste)),
Text(translate('accounts_menu.paste_invite'))
])
]).expanded()
])));
@ -191,8 +222,12 @@ class MainPagerState extends ConsumerState<MainPager>
// ignore: prefer_expression_function_bodies
Widget build(BuildContext context) {
final theme = Theme.of(context);
final textTheme = theme.textTheme;
final scale = theme.extension<ScaleScheme>()!;
return Scaffold(
extendBody: true,
backgroundColor: scale.grayScale.subtleBackground,
body: NotificationListener<ScrollNotification>(
onNotification: onScrollNotification,
child: PageView(

View File

@ -20,6 +20,44 @@ import 'account.dart';
part 'contact.g.dart';
Future<void> deleteContactInvitation(
{required ActiveAccountInfo activeAccountInfo,
required ContactInvitationRecord contactInvitationRecord}) async {
final pool = await DHTRecordPool.instance();
final accountRecordKey =
activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey;
// Remove ContactInvitationRecord from account's list
await (await DHTShortArray.openOwned(
proto.OwnedDHTRecordPointerProto.fromProto(
activeAccountInfo.account.contactInvitationRecords),
parent: accountRecordKey))
.scope((cirList) async {
for (var i = 0; i < cirList.length; i++) {
final item = await cirList.getItemProtobuf(
proto.ContactInvitationRecord.fromBuffer, i);
if (item == null) {
throw StateError('Failed to get contact invitation record');
}
if (item.contactRequestInbox.recordKey ==
contactInvitationRecord.contactRequestInbox.recordKey) {
await cirList.tryRemoveItem(i);
break;
}
}
await (await pool.openOwned(
proto.OwnedDHTRecordPointerProto.fromProto(
contactInvitationRecord.contactRequestInbox),
parent: accountRecordKey))
.delete();
await (await pool.openOwned(
proto.OwnedDHTRecordPointerProto.fromProto(
contactInvitationRecord.localConversation),
parent: accountRecordKey))
.delete();
});
}
Future<Uint8List> createContactInvitation(
{required ActiveAccountInfo activeAccountInfo,
required EncryptionKeyType encryptionKeyType,
@ -50,14 +88,14 @@ Future<Uint8List> createContactInvitation(
// identity key
late final Uint8List signedContactInvitationBytes;
await (await pool.create(parent: accountRecordKey))
.deleteScope((localChatRecord) async {
.deleteScope((localConversation) async {
// Make ContactRequestPrivate and encrypt with the writer secret
final crpriv = ContactRequestPrivate()
..writerKey = writer.key.toProto()
..profile = activeAccountInfo.account.profile
..accountMasterRecordKey =
activeAccountInfo.userLogin.accountMasterRecordKey.toProto()
..chatRecordKey = localChatRecord.key.toProto()
..chatRecordKey = localConversation.key.toProto()
..expiration = expiration?.toInt64() ?? Int64.ZERO;
final crprivbytes = crpriv.writeToBuffer();
final encryptedContactRequestPrivate =
@ -74,13 +112,13 @@ Future<Uint8List> createContactInvitation(
schema: DHTSchema.smpl(
oCnt: 1, members: [DHTSchemaMember(mCnt: 1, mKey: writer.key)]),
crypto: const DHTRecordCryptoPublic()))
.deleteScope((inboxRecord) async {
.deleteScope((contactRequestInbox) async {
// Store ContactRequest in owner subkey
await inboxRecord.eventualWriteProtobuf(creq);
await contactRequestInbox.eventualWriteProtobuf(creq);
// Create ContactInvitation and SignedContactInvitation
final cinv = ContactInvitation()
..contactRequestRecordKey = inboxRecord.key.toProto()
..contactRequestInboxKey = contactRequestInbox.key.toProto()
..writerSecret = encryptedSecret;
final cinvbytes = cinv.writeToBuffer();
final scinv = SignedContactInvitation()
@ -91,15 +129,16 @@ Future<Uint8List> createContactInvitation(
// Create ContactInvitationRecord
final cinvrec = ContactInvitationRecord()
..contactRequestRecordKey = inboxRecord.key.toProto()
..contactRequestInbox =
contactRequestInbox.ownedDHTRecordPointer.toProto()
..writerKey = writer.key.toProto()
..writerSecret = writer.secret.toProto()
..chatRecordKey = localChatRecord.key.toProto()
..localConversation = localConversation.ownedDHTRecordPointer.toProto()
..expiration = expiration?.toInt64() ?? Int64.ZERO
..invitation = signedContactInvitationBytes
..message = message;
// Add ContactInvitationRecord to local table if possible
// Add ContactInvitationRecord to account's list
// if this fails, don't keep retrying, user can try again later
await (await DHTShortArray.openOwned(
proto.OwnedDHTRecordPointerProto.fromProto(

View File

@ -1,15 +1,12 @@
import 'dart:async';
import 'dart:convert';
import 'dart:typed_data';
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import '../entities/entities.dart';
import '../entities/proto.dart' as proto;
import '../log/loggy.dart';
import '../tools/tools.dart';
import '../veilid_support/veilid_support.dart';
import 'account.dart';
import 'logins.dart';
part 'local_accounts.g.dart';
@ -35,7 +32,14 @@ class LocalAccounts extends _$LocalAccounts
/// Get all local account information
@override
FutureOr<IList<LocalAccount>> build() async => await load();
FutureOr<IList<LocalAccount>> build() async {
try {
return await load();
} on Exception catch (e) {
log.error('Failed to load LocalAccounts table: $e');
return const IListConst([]);
}
}
//////////////////////////////////////////////////////////////
/// Mutators and Selectors

View File

@ -112,7 +112,7 @@ class FetchLocalAccountProvider
}
}
String _$localAccountsHash() => r'80485dab3a2d1024fb5ffe29d9272dc4f3db2dff';
String _$localAccountsHash() => r'3f532f7a6caf8e4eaa9f8636a632126a10b8b07f';
/// See also [LocalAccounts].
@ProviderFor(LocalAccounts)

View File

@ -1,10 +1,10 @@
import 'dart:async';
import 'dart:convert';
import 'dart:typed_data';
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import '../entities/entities.dart';
import '../log/loggy.dart';
import '../veilid_support/veilid_support.dart';
import 'local_accounts.dart';
@ -28,7 +28,14 @@ class Logins extends _$Logins with AsyncTableDBBacked<ActiveLogins> {
/// Get all local account information
@override
FutureOr<ActiveLogins> build() async => await load();
FutureOr<ActiveLogins> build() async {
try {
return await load();
} on Exception catch (e) {
log.error('Failed to load ActiveLogins table: $e');
return const ActiveLogins(userLogins: IListConst([]));
}
}
//////////////////////////////////////////////////////////////
/// Mutators and Selectors

View File

@ -111,7 +111,7 @@ class FetchLoginProvider extends AutoDisposeFutureProvider<UserLogin?> {
}
}
String _$loginsHash() => r'fdabd035aaa7ae2521ed4b7d984b6ff41576f0ba';
String _$loginsHash() => r'b07a2fe61a8662dbeb5f12d823d49d3645b2b944';
/// See also [Logins].
@ProviderFor(Logins)

View File

@ -1,4 +1,3 @@
import 'dart:convert';
import 'dart:typed_data';
import '../entities/local_account.dart';
import '../veilid_support/veilid_support.dart';

View File

@ -151,15 +151,16 @@ class ScaleScheme extends ThemeExtension<ScaleScheme> {
ChatTheme toChatTheme() => DefaultChatTheme(
primaryColor: primaryScale.background,
secondaryColor: secondaryScale.background,
backgroundColor: grayScale.subtleBackground,
inputBackgroundColor: primaryScale.appBackground,
backgroundColor: grayScale.appBackground,
inputBackgroundColor: grayScale.subtleBackground,
inputBorderRadius: BorderRadius.zero,
inputTextDecoration: InputDecoration(
border: OutlineInputBorder(
borderSide: BorderSide(color: primaryScale.subtleBorder),
borderRadius: const BorderRadius.all(Radius.circular(16))),
),
inputContainerDecoration: BoxDecoration(color: grayScale.appBackground),
inputContainerDecoration:
BoxDecoration(color: primaryScale.appBackground),
inputPadding: EdgeInsets.all(5),
inputTextStyle: const TextStyle(
fontSize: 16,

View File

@ -1,6 +1,7 @@
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import '../../log/loggy.dart';
import '../veilid_support.dart';
part 'dht_record_pool.freezed.dart';
@ -76,7 +77,11 @@ class DHTRecordPool with AsyncTableDBBacked<DHTRecordPoolAllocations> {
.withSequencing(Sequencing.preferOrdered);
final globalPool = DHTRecordPool._(veilid, routingContext);
globalPool._state = await globalPool.load();
try {
globalPool._state = await globalPool.load();
} on Exception catch (e) {
log.error('Failed to load DHTRecordPool: $e');
}
_singleton = globalPool;
}
return _singleton!;

View File

@ -20,6 +20,14 @@ class _DHTShortArrayCache {
final List<DHTRecord> linkedRecords;
final List<int> index;
final List<int> free;
proto.DHTShortArray toProto() {
final head = proto.DHTShortArray();
head.keys.addAll(linkedRecords.map((lr) => lr.key.toProto()));
head.index = head.index..addAll(index);
// Do not serialize free list, it gets recreated
return head;
}
}
class DHTShortArray {
@ -134,9 +142,7 @@ class DHTShortArray {
/// if a newer copy is available online. Returns true if the write was
/// successful
Future<bool> _tryWriteHead() async {
final head = proto.DHTShortArray();
head.keys.addAll(_head.linkedRecords.map((lr) => lr.key.toProto()));
head.index.addAll(_head.index);
final head = _head.toProto();
final headBuffer = head.writeToBuffer();
final existingData = await _headRecord.tryWriteBytes(headBuffer);

View File

@ -75,9 +75,57 @@ class IdentityMasterWithSecrets {
});
}
/// Creates a new master identity and returns it with its secrets
/// Deletes a master identity and the identity record under it
Future<void> delete() async {
final pool = await DHTRecordPool.instance();
await pool.deleteDeep(identityMaster.masterRecordKey);
await (await pool.openRead(identityMaster.masterRecordKey)).delete();
}
}
/// Opens an existing master identity and validates it
Future<IdentityMaster> openIdentityMaster(
{required TypedKey identityMasterRecordKey}) async {
final pool = await DHTRecordPool.instance();
// IdentityMaster DHT record is public/unencrypted
return (await pool.openRead(identityMasterRecordKey))
.deleteScope((masterRec) async {
final identityMasterJson =
(await masterRec.getJson(IdentityMaster.fromJson, forceRefresh: true))!;
final identityMaster = IdentityMaster.fromJson(identityMasterJson);
// Validate IdentityMaster
final masterRecordKey = masterRec.key;
final masterOwnerKey = masterRec.owner;
final masterSigBuf = BytesBuilder()
..add(masterRecordKey.decode())
..add(masterOwnerKey.decode());
final masterSignature = identityMaster.masterSignature;
final identityRecordKey = identityMaster.identityRecordKey;
final identityOwnerKey = identityMaster.identityPublicKey;
final identitySigBuf = BytesBuilder()
..add(identityRecordKey.decode())
..add(identityOwnerKey.decode());
final identitySignature = identityMaster.identitySignature;
assert(masterRecordKey.kind == identityRecordKey.kind,
'new master and identity should have same cryptosystem');
final crypto = await pool.veilid.getCryptoSystem(masterRecordKey.kind);
await crypto.verify(
masterOwnerKey, identitySigBuf.toBytes(), identitySignature);
await crypto.verify(
identityOwnerKey, masterSigBuf.toBytes(), masterSignature);
return identityMaster;
});
}
extension IdentityMasterX on IdentityMaster {
/// Deletes a master identity and the identity record under it
Future<void> delete() async {
final pool = await DHTRecordPool.instance();
await (await pool.openRead(masterRecordKey)).delete();
}
}

View File

@ -103,6 +103,11 @@ flutter:
- assets/images/icon.svg
- assets/images/title.svg
- assets/images/vlogo.svg
# Fonts
fonts:
- family: Victor Mono
fonts:
- asset: assets/fonts/VictorMono-VariableFont_wght.ttf
# An image asset can refer to one or more resolution-specific "variants", see
# https://flutter.dev/assets-and-images/#resolution-aware