mirror of
https://gitlab.com/veilid/veilidchat.git
synced 2024-12-28 00:59:25 -05:00
accept via paste
This commit is contained in:
parent
7496a1a2a7
commit
8bb8285e50
BIN
assets/fonts/VictorMono-VariableFont_wght.ttf
Normal file
BIN
assets/fonts/VictorMono-VariableFont_wght.ttf
Normal file
Binary file not shown.
@ -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"
|
||||
|
@ -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,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
)));
|
||||
],
|
||||
),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
@ -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)));
|
||||
},
|
||||
),
|
||||
]));
|
||||
|
@ -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))));
|
||||
}
|
||||
}
|
||||
|
@ -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()
|
||||
],
|
||||
),
|
||||
);
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
251
lib/components/paste_invite_dialog.dart
Normal file
251
lib/components/paste_invite_dialog.dart
Normal 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));
|
||||
}
|
||||
}
|
@ -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
|
||||
|
@ -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'),
|
||||
|
@ -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);
|
||||
|
@ -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');
|
||||
|
||||
|
@ -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
|
||||
|
@ -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(),
|
||||
]);
|
||||
}
|
||||
|
||||
|
@ -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(
|
||||
|
@ -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(
|
||||
|
@ -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
|
||||
|
@ -112,7 +112,7 @@ class FetchLocalAccountProvider
|
||||
}
|
||||
}
|
||||
|
||||
String _$localAccountsHash() => r'80485dab3a2d1024fb5ffe29d9272dc4f3db2dff';
|
||||
String _$localAccountsHash() => r'3f532f7a6caf8e4eaa9f8636a632126a10b8b07f';
|
||||
|
||||
/// See also [LocalAccounts].
|
||||
@ProviderFor(LocalAccounts)
|
||||
|
@ -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
|
||||
|
@ -111,7 +111,7 @@ class FetchLoginProvider extends AutoDisposeFutureProvider<UserLogin?> {
|
||||
}
|
||||
}
|
||||
|
||||
String _$loginsHash() => r'fdabd035aaa7ae2521ed4b7d984b6ff41576f0ba';
|
||||
String _$loginsHash() => r'b07a2fe61a8662dbeb5f12d823d49d3645b2b944';
|
||||
|
||||
/// See also [Logins].
|
||||
@ProviderFor(Logins)
|
||||
|
@ -1,4 +1,3 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:typed_data';
|
||||
import '../entities/local_account.dart';
|
||||
import '../veilid_support/veilid_support.dart';
|
||||
|
@ -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,
|
||||
|
@ -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!;
|
||||
|
@ -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);
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user