contact invitation algorithm

This commit is contained in:
Christien Rioux 2023-08-02 21:09:28 -04:00
parent c35056f687
commit f52094c105
43 changed files with 1319 additions and 451 deletions

View File

@ -43,12 +43,37 @@
"account_page": {
"missing_account_title": "Missing Account",
"missing_account_text": "Account is missing, removing from list",
"invalid_account_title": "Missing Account",
"invalid_account_text": "Account is missing, removing from list"
"invalid_account_title": "Invalid Account",
"invalid_account_text": "Account is invalid, removing from list"
},
"empty_contact_list": {
"invite_people": "Invite people to VeilidChat"
},
"accounts_menu": {
"invite_contact": "Invite Contact",
"send_invite": "Send Invite",
"receive_invite": "Receive Invite"
},
"send_invite_dialog": {
"connect_with_me": "Connect with me on VeilidChat!",
"enter_message_hint": "enter message for contact (optional)",
"message_to_contact": "Message to send with invitation (not encrypted)",
"generate": "Generate Invite",
"message": "Message",
"unlocked": "Unlocked",
"numeric_pin": "Numeric PIN",
"password": "Password",
"protect_this_invitation": "Protect this invitation:",
"note": "Note:",
"note_text": "Contact invitations can be used by anyone. Make sure you send the invitation to your contact over a secure medium, and preferably use a password or pin to ensure that they are the only ones who can unlock the invitation and accept it.",
"pin_description": "Choose a PIN to protect the contact invite.\n\nThis level of security is appropriate only for casual connections in public environments for 'shoulder surfing' protection.",
"password_description": "Choose a strong password to protect the contact invite.\n\nThis level of security is appropriate when you must be sure the contact invitation is only accepted by its intended recipient. Share this password over a different medium than the invite itself.",
"pin_does_not_match": "PIN does not match"
},
"enter_pin_dialog": {
"enter_pin": "Enter PIN",
"reenter_pin": "Re-Enter PIN To Confirm"
},
"themes": {
"vapor": "Vapor"
}

View File

@ -3,12 +3,13 @@
2. Encrypt secret with requested encryption type
3. Create Local Chat DHT record (no content yet, will be encrypted with DH of contact identity key)
4. Create ContactRequestPrivate and encrypt with the writer secret
5. Create ContactRequest and embed possibly encrypted ContactRequestPrivate
5. Create ContactRequest and embed encrypted ContactRequestPrivate
6. Create DHT unicast inbox for ContactRequest and store ContactRequest in owner subkey
7. Create ContactInvitation and add invitation record to local table
7. Create ContactInvitation
8. Create SignedContactInvitation embedding ContactInvitation
9. Render SignedContactInvitation to shareable encoding (qr code, text blob, etc)
10. Share SignedContactInvitation out of band to desired contact, along with password somehow if used
9. Create ContactInvitationRecord and add to local table in Account
10. Render SignedContactInvitation to shareable encoding (qr code, text blob, etc)
11. Share SignedContactInvitation out of band to desired contact, along with password somehow if used
## Receiving an invitation
1. Receive SignedContactInvitation from out of band, and the password somehow if used

View File

@ -6,6 +6,8 @@ PODS:
- path_provider_foundation (0.0.1):
- Flutter
- FlutterMacOS
- share_plus (0.0.1):
- Flutter
- shared_preferences_foundation (0.0.1):
- Flutter
- FlutterMacOS
@ -14,15 +16,19 @@ PODS:
- FMDB (>= 2.7.5)
- system_info_plus (0.0.1):
- Flutter
- url_launcher_ios (0.0.1):
- Flutter
- veilid (0.0.1):
- Flutter
DEPENDENCIES:
- Flutter (from `Flutter`)
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
- share_plus (from `.symlinks/plugins/share_plus/ios`)
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
- sqflite (from `.symlinks/plugins/sqflite/ios`)
- system_info_plus (from `.symlinks/plugins/system_info_plus/ios`)
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
- veilid (from `.symlinks/plugins/veilid/ios`)
SPEC REPOS:
@ -34,12 +40,16 @@ EXTERNAL SOURCES:
:path: Flutter
path_provider_foundation:
:path: ".symlinks/plugins/path_provider_foundation/darwin"
share_plus:
:path: ".symlinks/plugins/share_plus/ios"
shared_preferences_foundation:
:path: ".symlinks/plugins/shared_preferences_foundation/darwin"
sqflite:
:path: ".symlinks/plugins/sqflite/ios"
system_info_plus:
:path: ".symlinks/plugins/system_info_plus/ios"
url_launcher_ios:
:path: ".symlinks/plugins/url_launcher_ios/ios"
veilid:
:path: ".symlinks/plugins/veilid/ios"
@ -47,9 +57,11 @@ SPEC CHECKSUMS:
Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854
FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a
path_provider_foundation: 29f094ae23ebbca9d3d0cec13889cd9060c0e943
share_plus: 599aa54e4ea31d4b4c0e9c911bcc26c55e791028
shared_preferences_foundation: 5b919d13b803cadd15ed2dc053125c68730e5126
sqflite: 31f7eba61e3074736dff8807a9b41581e4f7f15a
system_info_plus: 5393c8da281d899950d751713575fbf91c7709aa
url_launcher_ios: 08a3dfac5fb39e8759aeb0abbd5d9480f30fc8b4
veilid: f5c2e662f91907b30cf95762619526ac3e4512fd
PODFILE CHECKSUM: fcab1959fbc0528061dce4ed4f921740dc46f1e5

View File

@ -0,0 +1,67 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
class BottomSheetActionButton extends ConsumerStatefulWidget {
const BottomSheetActionButton(
{required this.bottomSheetBuilder,
required this.builder,
this.foregroundColor,
this.backgroundColor,
this.shape,
super.key});
final Color? foregroundColor;
final Color? backgroundColor;
final ShapeBorder? shape;
final Widget Function(BuildContext) builder;
final Widget Function(BuildContext) bottomSheetBuilder;
@override
BottomSheetActionButtonState createState() => BottomSheetActionButtonState();
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties
..add(ObjectFlagProperty<Widget Function(BuildContext p1)>.has(
'bottomSheetBuilder', bottomSheetBuilder))
..add(ColorProperty('foregroundColor', foregroundColor))
..add(ColorProperty('backgroundColor', backgroundColor))
..add(DiagnosticsProperty<ShapeBorder?>('shape', shape))
..add(ObjectFlagProperty<Widget? Function(BuildContext p1)>.has(
'builder', builder));
}
}
class BottomSheetActionButtonState
extends ConsumerState<BottomSheetActionButton> {
bool _showFab = true;
@override
void initState() {
super.initState();
}
@override
// ignore: prefer_expression_function_bodies
Widget build(BuildContext context) {
//
return _showFab
? FloatingActionButton(
shape: widget.shape,
foregroundColor: widget.foregroundColor,
backgroundColor: widget.backgroundColor,
child: widget.builder(context),
onPressed: () async {
await showModalBottomSheet<void>(
context: context, builder: widget.bottomSheetBuilder);
},
)
: Container();
}
void showFloatingActionButton(bool value) {
setState(() {
_showFab = value;
});
}
}

View File

@ -5,6 +5,8 @@ import 'package:flutter_chat_types/flutter_chat_types.dart' as types;
import 'package:intl/date_symbol_data_local.dart';
import 'package:uuid/uuid.dart';
import '../tools/theme_service.dart';
class ChatComponent extends ConsumerStatefulWidget {
const ChatComponent({super.key});
@ -63,56 +65,68 @@ class ChatComponentState extends ConsumerState<ChatComponent> {
_addMessage(textMessage);
}
void _handleAttachmentPressed() {
//
}
@override
// ignore: prefer_expression_function_bodies
Widget build(BuildContext context) {
final theme = Theme.of(context);
final scale = theme.extension<ScaleScheme>()!;
final chatTheme = scale.toChatTheme();
final textTheme = Theme.of(context).textTheme;
//
return Align(
alignment: AlignmentDirectional.centerEnd,
child: Container(
decoration: BoxDecoration(
color: Theme.of(context).scaffoldBackgroundColor,
),
child: Stack(
children: [
Column(
return DefaultTextStyle(
style: textTheme.bodySmall!,
child: Align(
alignment: AlignmentDirectional.centerEnd,
child: Container(
decoration: BoxDecoration(
color: scale.primaryScale.appBackground,
),
child: Stack(
children: [
Container(
height: 48,
decoration: BoxDecoration(
color: Theme.of(context).primaryColor,
),
child: Align(
alignment: AlignmentDirectional.centerStart,
child: Padding(
padding:
const EdgeInsetsDirectional.fromSTEB(16, 0, 16, 0),
child: Text("current contact",
textAlign: TextAlign.start,
style: Theme.of(context).textTheme.titleMedium),
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),
),
),
),
),
),
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,
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,
),
),
),
),
],
),
],
),
],
),
));
)));
}
}

View File

@ -1,25 +1,74 @@
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_animate/flutter_animate.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_translate/flutter_translate.dart';
class ContactInvitationDisplay extends ConsumerWidget {
const ContactInvitationDisplay({super.key});
//final LocalAccount account;
import '../tools/tools.dart';
class ContactInvitationDisplayDialog extends ConsumerStatefulWidget {
const ContactInvitationDisplayDialog({
super.key,
});
// EncryptionKeyType _encryptionKeyType = EncryptionKeyType.none;
// _encryptionKey = '';
@override
Widget build(BuildContext context, WidgetRef ref) {
//final logins = ref.watch(loginsProvider);
ContactInvitationDisplayDialogState createState() =>
ContactInvitationDisplayDialogState();
}
return ConstrainedBox(
constraints: const BoxConstraints(maxHeight: 300),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [const Expanded(child: Text('Contact Invitation'))]));
class ContactInvitationDisplayDialogState
extends ConsumerState<ContactInvitationDisplayDialog> {
final focusNode = FocusNode();
final formKey = GlobalKey<FormState>();
Future<void>? _generateFuture;
@override
void initState() {
super.initState();
if (_generateFuture == null) {
_generateFuture = _generate();
}
}
Future<void> _generate() async {
// Generate invitation
setState(() {
_generateFuture = null;
});
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
//properties.add(DiagnosticsProperty<LocalAccount>('account', account));
void dispose() {
focusNode.dispose();
super.dispose();
}
@override
// ignore: prefer_expression_function_bodies
Widget build(BuildContext context) {
final theme = Theme.of(context);
final scale = theme.extension<ScaleScheme>()!;
final cardsize = MediaQuery.of(context).size.shortestSide - 24;
//
return Dialog(
backgroundColor: Colors.white,
child: SizedBox(
width: cardsize,
height: cardsize,
child: Form(
key: formKey,
child: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: [Text("Contact Invitation")]))
.withModalHUD(context, _generateFuture != null)));
}
}

View File

@ -0,0 +1,162 @@
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:pinput/pinput.dart';
import '../tools/tools.dart';
class EnterPinDialog extends ConsumerStatefulWidget {
const EnterPinDialog({
this.matchPin,
this.description,
super.key,
});
final String? matchPin;
final String? description;
@override
EnterPinDialogState createState() => EnterPinDialogState();
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties
..add(StringProperty('matchPin', matchPin))
..add(StringProperty('description', description));
}
}
class EnterPinDialogState extends ConsumerState<EnterPinDialog> {
final pinController = TextEditingController();
final focusNode = FocusNode();
final formKey = GlobalKey<FormState>();
@override
void initState() {
super.initState();
}
@override
void dispose() {
pinController.dispose();
focusNode.dispose();
super.dispose();
}
@override
// ignore: prefer_expression_function_bodies
Widget build(BuildContext context) {
final theme = Theme.of(context);
final scale = theme.extension<ScaleScheme>()!;
final focusedBorderColor = scale.primaryScale.hoverBorder;
final fillColor = scale.primaryScale.elementBackground;
final borderColor = scale.primaryScale.border;
final defaultPinTheme = PinTheme(
width: 56,
height: 60,
textStyle: TextStyle(fontSize: 22, color: scale.primaryScale.text),
decoration: BoxDecoration(
color: fillColor,
borderRadius: BorderRadius.circular(8),
border: Border.all(color: borderColor),
),
);
/// Optionally you can use form to validate the Pinput
return Dialog(
backgroundColor: scale.grayScale.subtleBackground,
child: Form(
key: formKey,
child: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
widget.matchPin == null
? translate('enter_pin_dialog.enter_pin')
: translate('enter_pin_dialog.reenter_pin'),
style: theme.textTheme.titleLarge,
).paddingAll(16),
Directionality(
// Specify direction if desired
textDirection: TextDirection.ltr,
child: Pinput(
controller: pinController,
focusNode: focusNode,
autofocus: true,
defaultPinTheme: defaultPinTheme,
enableSuggestions: false,
inputFormatters: <TextInputFormatter>[
FilteringTextInputFormatter.digitsOnly
],
// validator: (widget.matchPin != null)
// ? (value) => value == widget.matchPin
// ? null
// : translate('enter_pin_dialog.pin_does_not_match')
// : null,
// onClipboardFound: (value) {
// debugPrint('onClipboardFound: $value');
// pinController.setText(value);
// },
hapticFeedbackType: HapticFeedbackType.lightImpact,
onCompleted: (pin) {
debugPrint('onCompleted: $pin');
Navigator.pop(context, pin);
},
onChanged: (value) {
debugPrint('onChanged: $value');
},
cursor: Column(
mainAxisAlignment: MainAxisAlignment.end,
children: [
Container(
margin: const EdgeInsets.only(bottom: 9),
width: 22,
height: 1,
color: focusedBorderColor,
),
],
),
focusedPinTheme: defaultPinTheme.copyWith(
height: 68,
width: 64,
decoration: defaultPinTheme.decoration!.copyWith(
border: Border.all(color: borderColor),
),
),
errorText: '',
errorPinTheme: defaultPinTheme.copyWith(
decoration: BoxDecoration(
color: scale.errorScale.border,
borderRadius: BorderRadius.circular(8),
),
),
).paddingAll(16),
),
if (widget.description != null)
SizedBox(
width: 400,
child: Text(
widget.description!,
textAlign: TextAlign.center,
).paddingAll(16))
],
),
));
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties
..add(DiagnosticsProperty<TextEditingController>(
'pinController', pinController))
..add(DiagnosticsProperty<FocusNode>('focusNode', focusNode))
..add(DiagnosticsProperty<GlobalKey<FormState>>('formKey', formKey));
}
}

View File

@ -0,0 +1,162 @@
import 'package:awesome_extensions/awesome_extensions.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 '../tools/tools.dart';
import 'contact_invitation_display.dart';
import 'enter_pin.dart';
class SendInviteDialog extends ConsumerStatefulWidget {
const SendInviteDialog({super.key});
@override
SendInviteDialogState createState() => SendInviteDialogState();
}
class SendInviteDialogState extends ConsumerState<SendInviteDialog> {
final messageTextController = TextEditingController(
text: translate('send_invite_dialog.connect_with_me'));
EncryptionKeyType _encryptionKeyType = EncryptionKeyType.none;
String _encryptionKey = '';
@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('send_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('send_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> _onGenerateButtonPressed() async {
await showDialog<void>(
context: context,
builder: (context) => ContactInvitationDisplayDialog());
// if (ret == null) {
// return;
// }
Navigator.of(context).pop();
}
@override
// ignore: prefer_expression_function_bodies
Widget build(BuildContext context) {
final theme = Theme.of(context);
//final scale = theme.extension<ScaleScheme>()!;
final textTheme = theme.textTheme;
return SizedBox(
height: 400,
child: SingleChildScrollView(
padding: const EdgeInsets.all(8),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Text(
translate('send_invite_dialog.message_to_contact'),
).paddingAll(8),
TextField(
controller: messageTextController,
decoration: InputDecoration(
border: const OutlineInputBorder(),
hintText: translate('send_invite_dialog.enter_message_hint'),
labelText: translate('send_invite_dialog.message')),
).paddingAll(8),
const SizedBox(height: 10),
Text(translate('send_invite_dialog.protect_this_invitation'),
style: textTheme.labelLarge)
.paddingAll(8),
Wrap(spacing: 5, children: [
ChoiceChip(
label: Text(translate('send_invite_dialog.unlocked')),
selected: _encryptionKeyType == EncryptionKeyType.none,
onSelected: _onNoneEncryptionSelected,
),
ChoiceChip(
label: Text(translate('send_invite_dialog.numeric_pin')),
selected: _encryptionKeyType == EncryptionKeyType.pin,
onSelected: _onPinEncryptionSelected,
),
ChoiceChip(
label: Text(translate('send_invite_dialog.password')),
selected: _encryptionKeyType == EncryptionKeyType.password,
onSelected: _onPasswordEncryptionSelected,
)
]).paddingAll(8),
Container(
width: double.infinity,
height: 60,
padding: const EdgeInsets.all(8),
child: ElevatedButton(
onPressed: _onGenerateButtonPressed,
child: Text(
translate('send_invite_dialog.generate'),
),
),
),
Text(translate('send_invite_dialog.note')).paddingAll(8),
Text(
translate('send_invite_dialog.note_text'),
style: Theme.of(context).textTheme.bodySmall,
).paddingAll(8),
],
),
),
);
}
}

View File

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

View File

@ -3,6 +3,7 @@ import 'dart:typed_data';
import 'package:change_case/change_case.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import '../entities/proto.dart' as proto;
import '../veilid_support/veilid_support.dart';
import 'identity.dart';
@ -22,7 +23,27 @@ enum EncryptionKeyType {
factory EncryptionKeyType.fromJson(dynamic j) =>
EncryptionKeyType.values.byName((j as String).toCamelCase());
factory EncryptionKeyType.fromProto(proto.EncryptionKeyType p) {
// ignore: exhaustive_cases
switch (p) {
case proto.EncryptionKeyType.ENCRYPTION_KEY_TYPE_NONE:
return EncryptionKeyType.none;
case proto.EncryptionKeyType.ENCRYPTION_KEY_TYPE_PIN:
return EncryptionKeyType.pin;
case proto.EncryptionKeyType.ENCRYPTION_KEY_TYPE_PASSWORD:
return EncryptionKeyType.password;
}
throw StateError('unknown EncryptionKeyType enum value');
}
String toJson() => name.toPascalCase();
proto.EncryptionKeyType toProto() => switch (this) {
EncryptionKeyType.none =>
proto.EncryptionKeyType.ENCRYPTION_KEY_TYPE_NONE,
EncryptionKeyType.pin =>
proto.EncryptionKeyType.ENCRYPTION_KEY_TYPE_PIN,
EncryptionKeyType.password =>
proto.EncryptionKeyType.ENCRYPTION_KEY_TYPE_PASSWORD,
};
}
// Local Accounts are stored in a table locally and not backed by a DHT key

View File

@ -1240,7 +1240,7 @@ class Account extends $pb.GeneratedMessage {
..aOB(2, _omitFieldNames ? '' : 'invisible')
..a<$core.int>(3, _omitFieldNames ? '' : 'autoAwayTimeoutSec', $pb.PbFieldType.OU3)
..aOM<OwnedDHTRecordPointer>(4, _omitFieldNames ? '' : 'contactList', subBuilder: OwnedDHTRecordPointer.create)
..aOM<OwnedDHTRecordPointer>(5, _omitFieldNames ? '' : 'contactRequests', subBuilder: OwnedDHTRecordPointer.create)
..aOM<OwnedDHTRecordPointer>(5, _omitFieldNames ? '' : 'contactInvitationRecords', subBuilder: OwnedDHTRecordPointer.create)
..hasRequiredFields = false
;
@ -1306,15 +1306,15 @@ class Account extends $pb.GeneratedMessage {
OwnedDHTRecordPointer ensureContactList() => $_ensure(3);
@$pb.TagNumber(5)
OwnedDHTRecordPointer get contactRequests => $_getN(4);
OwnedDHTRecordPointer get contactInvitationRecords => $_getN(4);
@$pb.TagNumber(5)
set contactRequests(OwnedDHTRecordPointer v) { setField(5, v); }
set contactInvitationRecords(OwnedDHTRecordPointer v) { setField(5, v); }
@$pb.TagNumber(5)
$core.bool hasContactRequests() => $_has(4);
$core.bool hasContactInvitationRecords() => $_has(4);
@$pb.TagNumber(5)
void clearContactRequests() => clearField(5);
void clearContactInvitationRecords() => clearField(5);
@$pb.TagNumber(5)
OwnedDHTRecordPointer ensureContactRequests() => $_ensure(4);
OwnedDHTRecordPointer ensureContactInvitationRecords() => $_ensure(4);
}
class ContactInvitation extends $pb.GeneratedMessage {
@ -1432,9 +1432,8 @@ class ContactRequest extends $pb.GeneratedMessage {
factory ContactRequest.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r);
static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'ContactRequest', createEmptyInstance: create)
..a<$core.List<$core.int>>(1, _omitFieldNames ? '' : 'writerSalt', $pb.PbFieldType.OY)
..e<EncryptionKind>(2, _omitFieldNames ? '' : 'encryptionKeyType', $pb.PbFieldType.OE, defaultOrMaker: EncryptionKind.ENCRYPTION_KIND_UNSPECIFIED, valueOf: EncryptionKind.valueOf, enumValues: EncryptionKind.values)
..a<$core.List<$core.int>>(3, _omitFieldNames ? '' : 'private', $pb.PbFieldType.OY)
..e<EncryptionKeyType>(1, _omitFieldNames ? '' : 'encryptionKeyType', $pb.PbFieldType.OE, defaultOrMaker: EncryptionKeyType.ENCRYPTION_KEY_TYPE_UNSPECIFIED, valueOf: EncryptionKeyType.valueOf, enumValues: EncryptionKeyType.values)
..a<$core.List<$core.int>>(2, _omitFieldNames ? '' : 'private', $pb.PbFieldType.OY)
..hasRequiredFields = false
;
@ -1460,31 +1459,22 @@ class ContactRequest extends $pb.GeneratedMessage {
static ContactRequest? _defaultInstance;
@$pb.TagNumber(1)
$core.List<$core.int> get writerSalt => $_getN(0);
EncryptionKeyType get encryptionKeyType => $_getN(0);
@$pb.TagNumber(1)
set writerSalt($core.List<$core.int> v) { $_setBytes(0, v); }
set encryptionKeyType(EncryptionKeyType v) { setField(1, v); }
@$pb.TagNumber(1)
$core.bool hasWriterSalt() => $_has(0);
$core.bool hasEncryptionKeyType() => $_has(0);
@$pb.TagNumber(1)
void clearWriterSalt() => clearField(1);
void clearEncryptionKeyType() => clearField(1);
@$pb.TagNumber(2)
EncryptionKind get encryptionKeyType => $_getN(1);
$core.List<$core.int> get private => $_getN(1);
@$pb.TagNumber(2)
set encryptionKeyType(EncryptionKind v) { setField(2, v); }
set private($core.List<$core.int> v) { $_setBytes(1, v); }
@$pb.TagNumber(2)
$core.bool hasEncryptionKeyType() => $_has(1);
$core.bool hasPrivate() => $_has(1);
@$pb.TagNumber(2)
void clearEncryptionKeyType() => clearField(2);
@$pb.TagNumber(3)
$core.List<$core.int> get private => $_getN(2);
@$pb.TagNumber(3)
set private($core.List<$core.int> v) { $_setBytes(2, v); }
@$pb.TagNumber(3)
$core.bool hasPrivate() => $_has(2);
@$pb.TagNumber(3)
void clearPrivate() => clearField(3);
void clearPrivate() => clearField(2);
}
class ContactRequestPrivate extends $pb.GeneratedMessage {
@ -1697,13 +1687,13 @@ class SignedContactResponse extends $pb.GeneratedMessage {
Signature ensureIdentitySignature() => $_ensure(1);
}
class ContactRequestRecord extends $pb.GeneratedMessage {
factory ContactRequestRecord() => create();
ContactRequestRecord._() : super();
factory ContactRequestRecord.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r);
factory ContactRequestRecord.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r);
class ContactInvitationRecord extends $pb.GeneratedMessage {
factory ContactInvitationRecord() => create();
ContactInvitationRecord._() : super();
factory ContactInvitationRecord.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r);
factory ContactInvitationRecord.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r);
static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'ContactRequestRecord', createEmptyInstance: create)
static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'ContactInvitationRecord', createEmptyInstance: create)
..aOM<TypedKey>(1, _omitFieldNames ? '' : 'contactRequestRecordKey', subBuilder: TypedKey.create)
..aOM<CryptoKey>(2, _omitFieldNames ? '' : 'writerKey', subBuilder: CryptoKey.create)
..aOM<CryptoKey>(3, _omitFieldNames ? '' : 'writerSecret', subBuilder: CryptoKey.create)
@ -1717,22 +1707,22 @@ class ContactRequestRecord extends $pb.GeneratedMessage {
'Using this can add significant overhead to your binary. '
'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '
'Will be removed in next major version')
ContactRequestRecord clone() => ContactRequestRecord()..mergeFromMessage(this);
ContactInvitationRecord clone() => ContactInvitationRecord()..mergeFromMessage(this);
@$core.Deprecated(
'Using this can add significant overhead to your binary. '
'Use [GeneratedMessageGenericExtensions.rebuild] instead. '
'Will be removed in next major version')
ContactRequestRecord copyWith(void Function(ContactRequestRecord) updates) => super.copyWith((message) => updates(message as ContactRequestRecord)) as ContactRequestRecord;
ContactInvitationRecord copyWith(void Function(ContactInvitationRecord) updates) => super.copyWith((message) => updates(message as ContactInvitationRecord)) as ContactInvitationRecord;
$pb.BuilderInfo get info_ => _i;
@$core.pragma('dart2js:noInline')
static ContactRequestRecord create() => ContactRequestRecord._();
ContactRequestRecord createEmptyInstance() => create();
static $pb.PbList<ContactRequestRecord> createRepeated() => $pb.PbList<ContactRequestRecord>();
static ContactInvitationRecord create() => ContactInvitationRecord._();
ContactInvitationRecord createEmptyInstance() => create();
static $pb.PbList<ContactInvitationRecord> createRepeated() => $pb.PbList<ContactInvitationRecord>();
@$core.pragma('dart2js:noInline')
static ContactRequestRecord getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<ContactRequestRecord>(create);
static ContactRequestRecord? _defaultInstance;
static ContactInvitationRecord getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<ContactInvitationRecord>(create);
static ContactInvitationRecord? _defaultInstance;
@$pb.TagNumber(1)
TypedKey get contactRequestRecordKey => $_getN(0);

View File

@ -51,21 +51,23 @@ class Availability extends $pb.ProtobufEnum {
const Availability._($core.int v, $core.String n) : super(v, n);
}
class EncryptionKind extends $pb.ProtobufEnum {
static const EncryptionKind ENCRYPTION_KIND_UNSPECIFIED = EncryptionKind._(0, _omitEnumNames ? '' : 'ENCRYPTION_KIND_UNSPECIFIED');
static const EncryptionKind ENCRYPTION_KIND_PIN = EncryptionKind._(1, _omitEnumNames ? '' : 'ENCRYPTION_KIND_PIN');
static const EncryptionKind ENCRYPTION_KIND_PASSWORD = EncryptionKind._(2, _omitEnumNames ? '' : 'ENCRYPTION_KIND_PASSWORD');
class EncryptionKeyType extends $pb.ProtobufEnum {
static const EncryptionKeyType ENCRYPTION_KEY_TYPE_UNSPECIFIED = EncryptionKeyType._(0, _omitEnumNames ? '' : 'ENCRYPTION_KEY_TYPE_UNSPECIFIED');
static const EncryptionKeyType ENCRYPTION_KEY_TYPE_NONE = EncryptionKeyType._(1, _omitEnumNames ? '' : 'ENCRYPTION_KEY_TYPE_NONE');
static const EncryptionKeyType ENCRYPTION_KEY_TYPE_PIN = EncryptionKeyType._(2, _omitEnumNames ? '' : 'ENCRYPTION_KEY_TYPE_PIN');
static const EncryptionKeyType ENCRYPTION_KEY_TYPE_PASSWORD = EncryptionKeyType._(3, _omitEnumNames ? '' : 'ENCRYPTION_KEY_TYPE_PASSWORD');
static const $core.List<EncryptionKind> values = <EncryptionKind> [
ENCRYPTION_KIND_UNSPECIFIED,
ENCRYPTION_KIND_PIN,
ENCRYPTION_KIND_PASSWORD,
static const $core.List<EncryptionKeyType> values = <EncryptionKeyType> [
ENCRYPTION_KEY_TYPE_UNSPECIFIED,
ENCRYPTION_KEY_TYPE_NONE,
ENCRYPTION_KEY_TYPE_PIN,
ENCRYPTION_KEY_TYPE_PASSWORD,
];
static final $core.Map<$core.int, EncryptionKind> _byValue = $pb.ProtobufEnum.initByValue(values);
static EncryptionKind? valueOf($core.int value) => _byValue[value];
static final $core.Map<$core.int, EncryptionKeyType> _byValue = $pb.ProtobufEnum.initByValue(values);
static EncryptionKeyType? valueOf($core.int value) => _byValue[value];
const EncryptionKind._($core.int v, $core.String n) : super(v, n);
const EncryptionKeyType._($core.int v, $core.String n) : super(v, n);
}

View File

@ -46,20 +46,22 @@ final $typed_data.Uint8List availabilityDescriptor = $convert.base64Decode(
'lMSVRZX09GRkxJTkUQARIVChFBVkFJTEFCSUxJVFlfRlJFRRACEhUKEUFWQUlMQUJJTElUWV9C'
'VVNZEAMSFQoRQVZBSUxBQklMSVRZX0FXQVkQBA==');
@$core.Deprecated('Use encryptionKindDescriptor instead')
const EncryptionKind$json = {
'1': 'EncryptionKind',
@$core.Deprecated('Use encryptionKeyTypeDescriptor instead')
const EncryptionKeyType$json = {
'1': 'EncryptionKeyType',
'2': [
{'1': 'ENCRYPTION_KIND_UNSPECIFIED', '2': 0},
{'1': 'ENCRYPTION_KIND_PIN', '2': 1},
{'1': 'ENCRYPTION_KIND_PASSWORD', '2': 2},
{'1': 'ENCRYPTION_KEY_TYPE_UNSPECIFIED', '2': 0},
{'1': 'ENCRYPTION_KEY_TYPE_NONE', '2': 1},
{'1': 'ENCRYPTION_KEY_TYPE_PIN', '2': 2},
{'1': 'ENCRYPTION_KEY_TYPE_PASSWORD', '2': 3},
],
};
/// Descriptor for `EncryptionKind`. Decode as a `google.protobuf.EnumDescriptorProto`.
final $typed_data.Uint8List encryptionKindDescriptor = $convert.base64Decode(
'Cg5FbmNyeXB0aW9uS2luZBIfChtFTkNSWVBUSU9OX0tJTkRfVU5TUEVDSUZJRUQQABIXChNFTk'
'NSWVBUSU9OX0tJTkRfUElOEAESHAoYRU5DUllQVElPTl9LSU5EX1BBU1NXT1JEEAI=');
/// Descriptor for `EncryptionKeyType`. Decode as a `google.protobuf.EnumDescriptorProto`.
final $typed_data.Uint8List encryptionKeyTypeDescriptor = $convert.base64Decode(
'ChFFbmNyeXB0aW9uS2V5VHlwZRIjCh9FTkNSWVBUSU9OX0tFWV9UWVBFX1VOU1BFQ0lGSUVEEA'
'ASHAoYRU5DUllQVElPTl9LRVlfVFlQRV9OT05FEAESGwoXRU5DUllQVElPTl9LRVlfVFlQRV9Q'
'SU4QAhIgChxFTkNSWVBUSU9OX0tFWV9UWVBFX1BBU1NXT1JEEAM=');
@$core.Deprecated('Use cryptoKeyDescriptor instead')
const CryptoKey$json = {
@ -344,7 +346,7 @@ const Account$json = {
{'1': 'invisible', '3': 2, '4': 1, '5': 8, '10': 'invisible'},
{'1': 'auto_away_timeout_sec', '3': 3, '4': 1, '5': 13, '10': 'autoAwayTimeoutSec'},
{'1': 'contact_list', '3': 4, '4': 1, '5': 11, '6': '.OwnedDHTRecordPointer', '10': 'contactList'},
{'1': 'contact_requests', '3': 5, '4': 1, '5': 11, '6': '.OwnedDHTRecordPointer', '10': 'contactRequests'},
{'1': 'contact_invitation_records', '3': 5, '4': 1, '5': 11, '6': '.OwnedDHTRecordPointer', '10': 'contactInvitationRecords'},
],
};
@ -353,8 +355,8 @@ final $typed_data.Uint8List accountDescriptor = $convert.base64Decode(
'CgdBY2NvdW50EiIKB3Byb2ZpbGUYASABKAsyCC5Qcm9maWxlUgdwcm9maWxlEhwKCWludmlzaW'
'JsZRgCIAEoCFIJaW52aXNpYmxlEjEKFWF1dG9fYXdheV90aW1lb3V0X3NlYxgDIAEoDVISYXV0'
'b0F3YXlUaW1lb3V0U2VjEjkKDGNvbnRhY3RfbGlzdBgEIAEoCzIWLk93bmVkREhUUmVjb3JkUG'
'9pbnRlclILY29udGFjdExpc3QSQQoQY29udGFjdF9yZXF1ZXN0cxgFIAEoCzIWLk93bmVkREhU'
'UmVjb3JkUG9pbnRlclIPY29udGFjdFJlcXVlc3Rz');
'9pbnRlclILY29udGFjdExpc3QSVAoaY29udGFjdF9pbnZpdGF0aW9uX3JlY29yZHMYBSABKAsy'
'Fi5Pd25lZERIVFJlY29yZFBvaW50ZXJSGGNvbnRhY3RJbnZpdGF0aW9uUmVjb3Jkcw==');
@$core.Deprecated('Use contactInvitationDescriptor instead')
const ContactInvitation$json = {
@ -390,17 +392,15 @@ final $typed_data.Uint8List signedContactInvitationDescriptor = $convert.base64D
const ContactRequest$json = {
'1': 'ContactRequest',
'2': [
{'1': 'writer_salt', '3': 1, '4': 1, '5': 12, '10': 'writerSalt'},
{'1': 'encryption_key_type', '3': 2, '4': 1, '5': 14, '6': '.EncryptionKind', '10': 'encryptionKeyType'},
{'1': 'private', '3': 3, '4': 1, '5': 12, '10': 'private'},
{'1': 'encryption_key_type', '3': 1, '4': 1, '5': 14, '6': '.EncryptionKeyType', '10': 'encryptionKeyType'},
{'1': 'private', '3': 2, '4': 1, '5': 12, '10': 'private'},
],
};
/// Descriptor for `ContactRequest`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List contactRequestDescriptor = $convert.base64Decode(
'Cg5Db250YWN0UmVxdWVzdBIfCgt3cml0ZXJfc2FsdBgBIAEoDFIKd3JpdGVyU2FsdBI/ChNlbm'
'NyeXB0aW9uX2tleV90eXBlGAIgASgOMg8uRW5jcnlwdGlvbktpbmRSEWVuY3J5cHRpb25LZXlU'
'eXBlEhgKB3ByaXZhdGUYAyABKAxSB3ByaXZhdGU=');
'Cg5Db250YWN0UmVxdWVzdBJCChNlbmNyeXB0aW9uX2tleV90eXBlGAEgASgOMhIuRW5jcnlwdG'
'lvbktleVR5cGVSEWVuY3J5cHRpb25LZXlUeXBlEhgKB3ByaXZhdGUYAiABKAxSB3ByaXZhdGU=');
@$core.Deprecated('Use contactRequestPrivateDescriptor instead')
const ContactRequestPrivate$json = {
@ -453,9 +453,9 @@ final $typed_data.Uint8List signedContactResponseDescriptor = $convert.base64Dec
'FjdFJlc3BvbnNlEjkKEmlkZW50aXR5X3NpZ25hdHVyZRgCIAEoCzIKLlNpZ25hdHVyZVIRaWRl'
'bnRpdHlTaWduYXR1cmU=');
@$core.Deprecated('Use contactRequestRecordDescriptor instead')
const ContactRequestRecord$json = {
'1': 'ContactRequestRecord',
@$core.Deprecated('Use contactInvitationRecordDescriptor instead')
const ContactInvitationRecord$json = {
'1': 'ContactInvitationRecord',
'2': [
{'1': 'contact_request_record_key', '3': 1, '4': 1, '5': 11, '6': '.TypedKey', '10': 'contactRequestRecordKey'},
{'1': 'writer_key', '3': 2, '4': 1, '5': 11, '6': '.CryptoKey', '10': 'writerKey'},
@ -466,12 +466,12 @@ const ContactRequestRecord$json = {
],
};
/// Descriptor for `ContactRequestRecord`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List contactRequestRecordDescriptor = $convert.base64Decode(
'ChRDb250YWN0UmVxdWVzdFJlY29yZBJGChpjb250YWN0X3JlcXVlc3RfcmVjb3JkX2tleRgBIA'
'EoCzIJLlR5cGVkS2V5Uhdjb250YWN0UmVxdWVzdFJlY29yZEtleRIpCgp3cml0ZXJfa2V5GAIg'
'ASgLMgouQ3J5cHRvS2V5Ugl3cml0ZXJLZXkSLwoNd3JpdGVyX3NlY3JldBgDIAEoCzIKLkNyeX'
'B0b0tleVIMd3JpdGVyU2VjcmV0EjEKD2NoYXRfcmVjb3JkX2tleRgEIAEoCzIJLlR5cGVkS2V5'
'Ug1jaGF0UmVjb3JkS2V5Eh4KCmV4cGlyYXRpb24YBSABKARSCmV4cGlyYXRpb24SHgoKaW52aX'
'RhdGlvbhgGIAEoDFIKaW52aXRhdGlvbg==');
/// Descriptor for `ContactInvitationRecord`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List contactInvitationRecordDescriptor = $convert.base64Decode(
'ChdDb250YWN0SW52aXRhdGlvblJlY29yZBJGChpjb250YWN0X3JlcXVlc3RfcmVjb3JkX2tleR'
'gBIAEoCzIJLlR5cGVkS2V5Uhdjb250YWN0UmVxdWVzdFJlY29yZEtleRIpCgp3cml0ZXJfa2V5'
'GAIgASgLMgouQ3J5cHRvS2V5Ugl3cml0ZXJLZXkSLwoNd3JpdGVyX3NlY3JldBgDIAEoCzIKLk'
'NyeXB0b0tleVIMd3JpdGVyU2VjcmV0EjEKD2NoYXRfcmVjb3JkX2tleRgEIAEoCzIJLlR5cGVk'
'S2V5Ug1jaGF0UmVjb3JkS2V5Eh4KCmV4cGlyYXRpb24YBSABKARSCmV4cGlyYXRpb24SHgoKaW'
'52aXRhdGlvbhgGIAEoDFIKaW52aXRhdGlvbg==');

View File

@ -258,17 +258,18 @@ message Account {
// The contacts DHTList for this account
// DHT Private
OwnedDHTRecordPointer contact_list = 4;
// The contact requests DHTShortArray for this account
// The ContactInvitationRecord DHTShortArray for this account
// DHT Private
OwnedDHTRecordPointer contact_requests = 5;
OwnedDHTRecordPointer contact_invitation_records = 5;
}
// EncryptionKind
// EncryptionKeyType
// Encryption of secret
enum EncryptionKind {
ENCRYPTION_KIND_UNSPECIFIED = 0;
ENCRYPTION_KIND_PIN = 1;
ENCRYPTION_KIND_PASSWORD =2;
enum EncryptionKeyType {
ENCRYPTION_KEY_TYPE_UNSPECIFIED = 0;
ENCRYPTION_KEY_TYPE_NONE = 1;
ENCRYPTION_KEY_TYPE_PIN = 2;
ENCRYPTION_KEY_TYPE_PASSWORD = 3;
}
// Invitation that is shared for VeilidChat contact connections
@ -277,7 +278,7 @@ enum EncryptionKind {
message ContactInvitation {
// Contact request DHT record key
TypedKey contact_request_record_key = 1;
// Writer secret key bytes possibly encrypted
// Writer secret key bytes possibly encrypted with nonce appended
bytes writer_secret = 2;
}
@ -290,14 +291,12 @@ message SignedContactInvitation {
}
// Contact request unicastinbox on the DHT
// DHTSchema: SMPL 2 owner key, 1 writer key symmetrically encrypted with writer secret
// DHTSchema: SMPL 1 owner key, 1 writer key symmetrically encrypted with writer secret
message ContactRequest {
// The salt for the encryption used on the unicastinbox writer secret
bytes writer_salt = 1;
// The kind of encryption used on the unicastinbox writer key
EncryptionKind encryption_key_type = 2;
EncryptionKeyType encryption_key_type = 1;
// The private part encoded and symmetrically encrypted with the unicastinbox writer secret
bytes private = 3;
bytes private = 2;
}
// The private part of a possibly encrypted contact request
@ -335,7 +334,7 @@ message SignedContactResponse {
}
// Contact request record kept in Account DHTList to keep track of extant contact invitations
message ContactRequestRecord {
message ContactInvitationRecord {
// Contact request unicastinbox DHT record key
TypedKey contact_request_record_key = 1;
// Unencrypted writer key for this request
@ -345,6 +344,6 @@ message ContactRequestRecord {
TypedKey chat_record_key = 4;
// Expiration timestamp
uint64 expiration = 5;
// A copy of the raw invitation bytes post-encryption
// A copy of the raw SignedContactResponse invitation bytes post-encryption and signing
bytes invitation = 6;
}

View File

@ -5,6 +5,8 @@ import 'package:split_view/split_view.dart';
import 'package:signal_strength_indicator/signal_strength_indicator.dart';
import '../components/chat_component.dart';
import '../providers/local_accounts.dart';
import '../providers/logins.dart';
import '../providers/window_control.dart';
import '../tools/tools.dart';
import 'main_pager/main_pager.dart';

View File

@ -1,12 +1,19 @@
import 'dart:async';
import 'package:awesome_extensions/awesome_extensions.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart';
import 'package:flutter_animate/flutter_animate.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_translate/flutter_translate.dart';
import 'package:stylish_bottom_bar/model/bar_items.dart';
import 'package:stylish_bottom_bar/stylish_bottom_bar.dart';
import '../../components/bottom_sheet_action_button.dart';
import '../../components/contact_invitation_display.dart';
import '../../components/send_invite_dialog.dart';
import '../../tools/tools.dart';
import 'account_page.dart';
import 'chats_page.dart';
@ -102,22 +109,81 @@ class MainPagerState extends ConsumerState<MainPager>
return bottomBarItems;
}
Future<void> _onNewContactInvitation(BuildContext context) async {
Scaffold.of(context).showBottomSheet<void>((context) => SizedBox(
height: 200, child: Center(child: ContactInvitationDisplay())));
Future<void> sendContactInvitationDialog(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(
'Send Contact Invite',
style: TextStyle(fontSize: 24),
),
content: SendInviteDialog());
});
}
Future<void> _onNewChat(BuildContext context) async {
//
Widget _newContactInvitationBottomSheetBuilder(
// ignore: prefer_expression_function_bodies
BuildContext context) {
return KeyboardListener(
focusNode: FocusNode(),
onKeyEvent: (ke) {
if (ke.logicalKey == LogicalKeyboardKey.escape) {
Navigator.pop(context);
}
},
child: SizedBox(
height: 200,
child: Column(children: [
Text(translate('accounts_menu.invite_contact'),
style: Theme.of(context).textTheme.titleMedium)
.paddingAll(8),
Row(mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [
Column(mainAxisAlignment: MainAxisAlignment.center, children: [
IconButton(
onPressed: () async {
Navigator.pop(context);
await sendContactInvitationDialog(context);
},
iconSize: 64,
icon: const Icon(Icons.output)),
Text(translate('accounts_menu.send_invite'))
]),
Column(mainAxisAlignment: MainAxisAlignment.center, children: [
IconButton(
onPressed: () {
Navigator.pop(context);
},
iconSize: 64,
icon: const Icon(Icons.input)),
Text(translate('accounts_menu.receive_invite'))
])
]).expanded()
])));
}
Future<void> _onFloatingActionButtonPressed(BuildContext context) async {
// ignore: prefer_expression_function_bodies
Widget _onNewChatBottomSheetBuilder(BuildContext context) {
return const SizedBox(height: 200, child: Center(child: Text("test")));
}
Widget _bottomSheetBuilder(BuildContext context) {
if (_currentPage == 0) {
// New contact invitation
return _onNewContactInvitation(context);
return _newContactInvitationBottomSheetBuilder(context);
} else if (_currentPage == 1) {
// New chat
return _onNewChat(context);
return _onNewChatBottomSheetBuilder(context);
} else {
// Unknown error
return waitingPage(context);
}
}
@ -151,6 +217,7 @@ class MainPagerState extends ConsumerState<MainPager>
// theme.colorScheme.primary,
// theme.colorScheme.primaryContainer,
// ]),
//borderRadius: BorderRadius.all(Radius.circular(16)),
option: AnimatedBarOptions(
// iconSize: 32,
//barAnimation: BarAnimation.fade,
@ -172,16 +239,16 @@ class MainPagerState extends ConsumerState<MainPager>
},
),
floatingActionButton: FloatingActionButton(
floatingActionButton: BottomSheetActionButton(
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(14))),
//foregroundColor: theme.colorScheme.secondary,
backgroundColor: theme.colorScheme.secondaryContainer,
child: Icon(
_fabIconList[_currentPage],
color: theme.colorScheme.onSecondaryContainer,
),
onPressed: () async => _onFloatingActionButtonPressed(context)),
builder: (context) => Icon(
_fabIconList[_currentPage],
color: theme.colorScheme.onSecondaryContainer,
),
bottomSheetBuilder: _bottomSheetBuilder),
floatingActionButtonLocation: FloatingActionButtonLocation.endDocked,
);
}

View File

@ -1,6 +1,8 @@
import 'package:riverpod_annotation/riverpod_annotation.dart';
import '../entities/local_account.dart';
import '../entities/proto.dart' as proto;
import '../entities/user_login.dart';
import '../veilid_support/veilid_support.dart';
import 'local_accounts.dart';
@ -70,3 +72,61 @@ Future<AccountInfo> fetchAccount(FetchAccountRef ref,
return AccountInfo(
status: AccountInfoStatus.accountReady, active: active, account: account);
}
class ActiveAccountInfo {
ActiveAccountInfo({
required this.localAccount,
required this.userLogin,
required this.account,
});
LocalAccount localAccount;
UserLogin userLogin;
proto.Account account;
}
/// Get the active account info
@riverpod
Future<ActiveAccountInfo?> fetchActiveAccount(FetchAccountRef ref) async {
// See if we've logged into this account or if it is locked
final activeUserLogin = await ref.watch(loginsProvider.future
.select((value) async => (await value).activeUserLogin));
if (activeUserLogin == null) {
return null;
}
// Get the user login
final userLogin = await ref.watch(
fetchLoginProvider(accountMasterRecordKey: activeUserLogin).future);
if (userLogin == null) {
// Account was locked
return null;
}
// Get which local account we want to fetch the profile for
final localAccount = await ref.watch(
fetchLocalAccountProvider(accountMasterRecordKey: activeUserLogin)
.future);
if (localAccount == null) {
// Local account does not exist
return null;
}
// Pull the account DHT key, decode it and return it
final pool = await DHTRecordPool.instance();
final account = await (await pool.openOwned(
userLogin.accountRecordInfo.accountRecord,
parent: localAccount.identityMaster.identityRecordKey))
.scope((accountRec) => accountRec.getProtobuf(proto.Account.fromBuffer));
if (account == null) {
// Account could not be read or decrypted from DHT
return null;
}
// Got account, decrypted and decoded
return ActiveAccountInfo(
localAccount: localAccount,
userLogin: userLogin,
account: account,
);
}

View File

@ -128,4 +128,25 @@ class FetchAccountProvider extends AutoDisposeFutureProvider<AccountInfo> {
return _SystemHash.finish(hash);
}
}
String _$fetchActiveAccountHash() =>
r'8c7e571c135deeb5cacf56c61459d71f7447baaf';
/// Get the active account info
///
/// Copied from [fetchActiveAccount].
@ProviderFor(fetchActiveAccount)
final fetchActiveAccountProvider =
AutoDisposeFutureProvider<ActiveAccountInfo?>.internal(
fetchActiveAccount,
name: r'fetchActiveAccountProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
? null
: _$fetchActiveAccountHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef FetchActiveAccountRef
= AutoDisposeFutureProviderRef<ActiveAccountInfo?>;
// ignore_for_file: unnecessary_raw_strings, subtype_of_sealed_class, invalid_use_of_internal_member, do_not_use_environment, prefer_const_constructors, public_member_api_docs, avoid_private_typedef_functions

103
lib/providers/contact.dart Normal file
View File

@ -0,0 +1,103 @@
import 'dart:typed_data';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import '../entities/local_account.dart';
import '../entities/proto.dart' as proto;
import '../entities/user_login.dart';
import '../tools/tools.dart';
import '../veilid_support/veilid_support.dart';
import 'account.dart';
Future<Uint8List> createContactInvitation(
ActiveAccountInfo activeAccountInfo,
EncryptionKeyType encryptionKeyType,
String encryptionKey,
Timestamp expiration) async {
final pool = await DHTRecordPool.instance();
final accountRecordKey =
activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey;
final identityKey =
activeAccountInfo.localAccount.identityMaster.identityPublicKey;
final identitySecret = activeAccountInfo.userLogin.identitySecret.value;
// Generate writer keypair to share with new contact
final cs = await pool.veilid.bestCryptoSystem();
final writer = await cs.generateKeyPair();
// Encrypt the writer secret with the encryption key
final encryptedSecret = await encryptSecretToBytes(
secret: writer.secret,
cryptoKind: cs.kind(),
encryptionKey: encryptionKey,
encryptionKeyType: encryptionKeyType);
// Create local chat DHT record with the account record key as its parent
// Do not set the encryption of this key yet as it will not yet be written
// to and it will be eventually encrypted with the DH of the contact's
// identity key
late final Uint8List signedContactInvitationBytes;
await (await pool.create(parent: accountRecordKey))
.deleteScope((localChatRecord) async {
// Make ContactRequestPrivate and encrypt with the writer secret
final crpriv = proto.ContactRequestPrivate()
..writerKey = writer.key.toProto()
..profile = activeAccountInfo.account.profile
..accountMasterRecordKey =
activeAccountInfo.userLogin.accountMasterRecordKey.toProto()
..chatRecordKey = localChatRecord.key.toProto()
..expiration = expiration.toInt64();
final crprivbytes = crpriv.writeToBuffer();
final encryptedContactRequestPrivate =
await cs.encryptNoAuthWithNonce(crprivbytes, writer.secret);
// Create ContactRequest and embed contactrequestprivate
final creq = proto.ContactRequest()
..encryptionKeyType = encryptionKeyType.toProto()
..private = encryptedContactRequestPrivate;
// Create DHT unicast inbox for ContactRequest
await (await pool.create(
parent: accountRecordKey,
schema: DHTSchema.smpl(
oCnt: 1, members: [DHTSchemaMember(mCnt: 1, mKey: writer.key)]),
crypto: const DHTRecordCryptoPublic()))
.deleteScope((inboxRecord) async {
// Store ContactRequest in owner subkey
await inboxRecord.eventualWriteProtobuf(creq);
// Create ContactInvitation and SignedContactInvitation
final cinv = proto.ContactInvitation()
..contactRequestRecordKey = inboxRecord.key.toProto()
..writerSecret = encryptedSecret;
final cinvbytes = cinv.writeToBuffer();
final scinv = proto.SignedContactInvitation()
..contactInvitation = cinvbytes
..identitySignature =
(await cs.sign(identityKey, identitySecret, cinvbytes)).toProto();
signedContactInvitationBytes = scinv.writeToBuffer();
// Create ContactInvitationRecord
final cinvrec = proto.ContactInvitationRecord()
..contactRequestRecordKey = inboxRecord.key.toProto()
..writerKey = writer.key.toProto()
..writerSecret = writer.secret.toProto()
..chatRecordKey = localChatRecord.key.toProto()
..expiration = expiration.toInt64()
..invitation = signedContactInvitationBytes;
// Add ContactInvitationRecord to local table
await (await DHTShortArray.openOwned(
proto.OwnedDHTRecordPointerProto.fromProto(
activeAccountInfo.account.contactInvitationRecords),
parent: accountRecordKey))
.scope((cirList) async {
if (await cirList.tryAddItem(cinvrec.writeToBuffer()) == false) {
throw StateError('Failed to add contact invitation record');
}
});
});
});
return signedContactInvitationBytes;
}

View File

@ -51,33 +51,6 @@ class LocalAccounts extends _$LocalAccounts
state = AsyncValue.data(updated);
}
/// Make encrypted identitySecret
Future<Uint8List> _encryptIdentitySecret(
{required SecretKey identitySecret,
required CryptoKind cryptoKind,
EncryptionKeyType encryptionKeyType = EncryptionKeyType.none,
String encryptionKey = ''}) async {
final veilid = await eventualVeilid.future;
late final Uint8List identitySecretBytes;
switch (encryptionKeyType) {
case EncryptionKeyType.none:
identitySecretBytes = identitySecret.decode();
case EncryptionKeyType.pin:
case EncryptionKeyType.password:
final cs = await veilid.getCryptoSystem(cryptoKind);
final ekbytes = Uint8List.fromList(utf8.encode(encryptionKey));
final nonce = await cs.randomNonce();
final identitySecretSaltBytes = nonce.decode();
final sharedSecret =
await cs.deriveSharedSecret(ekbytes, identitySecretSaltBytes);
identitySecretBytes = (await cs.cryptNoAuth(
identitySecret.decode(), nonce, sharedSecret))
..addAll(identitySecretSaltBytes);
}
return identitySecretBytes;
}
/// Creates a new Account associated with master identity
/// Adds a logged-out LocalAccount to track its existence on this device
Future<LocalAccount> newLocalAccount(
@ -97,8 +70,8 @@ class LocalAccounts extends _$LocalAccounts
);
// Encrypt identitySecret with key
final identitySecretBytes = await _encryptIdentitySecret(
identitySecret: identitySecret,
final identitySecretBytes = await encryptSecretToBytes(
secret: identitySecret,
cryptoKind: identityMaster.identityRecordKey.kind,
encryptionKey: encryptionKey,
encryptionKeyType: encryptionKeyType);

View File

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

View File

@ -121,18 +121,10 @@ class Logins extends _$Logins with AsyncTableDBBacked<ActiveLogins> {
}
final cs = await veilid
.getCryptoSystem(localAccount.identityMaster.identityRecordKey.kind);
final encryptionKeyBytes = Uint8List.fromList(utf8.encode(encryptionKey));
final identitySecretKeyBytes =
localAccount.identitySecretBytes.sublist(0, SecretKey.decodedLength());
final identitySecretSaltBytes =
localAccount.identitySecretBytes.sublist(SecretKey.decodedLength());
final nonce = Nonce.fromBytes(identitySecretSaltBytes);
final sharedSecret = await cs.deriveSharedSecret(
encryptionKeyBytes, identitySecretSaltBytes);
final identitySecret = SecretKey.fromBytes(
await cs.cryptNoAuth(identitySecretKeyBytes, nonce, sharedSecret));
await cs.decryptNoAuthWithPassword(
localAccount.identitySecretBytes, encryptionKey));
// Validate this secret with the identity public key and log in
return _loginCommon(localAccount.identityMaster, identitySecret);

View File

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

View File

@ -472,50 +472,48 @@ RadixScheme _radixScheme(Brightness brightness, RadixThemeColor themeColor) {
return radixScheme;
}
ExtendedColorScheme _radixColorScheme(
Brightness brightness, RadixThemeColor themeColor) {
final radix = _radixScheme(brightness, themeColor);
return ExtendedColorScheme(
scaleScheme: radix.toScale(),
brightness: brightness,
primary: radix.primaryScale.step9,
onPrimary: radix.primaryScale.step12,
primaryContainer: radix.primaryScale.step4,
onPrimaryContainer: radix.primaryScale.step11,
secondary: radix.secondaryScale.step9,
onSecondary: radix.secondaryScale.step12,
secondaryContainer: radix.secondaryScale.step3,
onSecondaryContainer: radix.secondaryScale.step11,
tertiary: radix.tertiaryScale.step9,
onTertiary: radix.tertiaryScale.step12,
tertiaryContainer: radix.tertiaryScale.step3,
onTertiaryContainer: radix.tertiaryScale.step11,
error: radix.errorScale.step9,
onError: radix.errorScale.step12,
errorContainer: radix.errorScale.step3,
onErrorContainer: radix.errorScale.step11,
background: radix.grayScale.step1,
onBackground: radix.grayScale.step11,
surface: radix.primaryScale.step1,
onSurface: radix.primaryScale.step12,
surfaceVariant: radix.secondaryScale.step2,
onSurfaceVariant: radix.secondaryScale.step11,
outline: radix.primaryScale.step7,
outlineVariant: radix.primaryScale.step6,
shadow: RadixColors.dark.gray.step1,
scrim: radix.primaryScale.step9,
inverseSurface: radix.primaryScale.step11,
onInverseSurface: radix.primaryScale.step2,
inversePrimary: radix.primaryScale.step10,
surfaceTint: radix.primaryAlphaScale.step4,
);
}
ColorScheme _radixColorScheme(Brightness brightness, RadixScheme radix) =>
ColorScheme(
brightness: brightness,
primary: radix.primaryScale.step9,
onPrimary: radix.primaryScale.step12,
primaryContainer: radix.primaryScale.step4,
onPrimaryContainer: radix.primaryScale.step11,
secondary: radix.secondaryScale.step9,
onSecondary: radix.secondaryScale.step12,
secondaryContainer: radix.secondaryScale.step3,
onSecondaryContainer: radix.secondaryScale.step11,
tertiary: radix.tertiaryScale.step9,
onTertiary: radix.tertiaryScale.step12,
tertiaryContainer: radix.tertiaryScale.step3,
onTertiaryContainer: radix.tertiaryScale.step11,
error: radix.errorScale.step9,
onError: radix.errorScale.step12,
errorContainer: radix.errorScale.step3,
onErrorContainer: radix.errorScale.step11,
background: radix.grayScale.step1,
onBackground: radix.grayScale.step11,
surface: radix.primaryScale.step1,
onSurface: radix.primaryScale.step12,
surfaceVariant: radix.secondaryScale.step2,
onSurfaceVariant: radix.secondaryScale.step11,
outline: radix.primaryScale.step7,
outlineVariant: radix.primaryScale.step6,
shadow: RadixColors.dark.gray.step1,
scrim: radix.primaryScale.step9,
inverseSurface: radix.primaryScale.step11,
onInverseSurface: radix.primaryScale.step2,
inversePrimary: radix.primaryScale.step10,
surfaceTint: radix.primaryAlphaScale.step4,
);
ThemeData radixGenerator(Brightness brightness, RadixThemeColor themeColor) {
TextTheme? textTheme;
final radix = _radixScheme(brightness, themeColor);
final colorScheme = _radixColorScheme(brightness, radix);
return ThemeData.from(
colorScheme: _radixColorScheme(brightness, themeColor),
textTheme: textTheme,
useMaterial3: true);
colorScheme: colorScheme, textTheme: textTheme, useMaterial3: true)
.copyWith(extensions: <ThemeExtension<dynamic>>[
radix.toScale(),
]);
}

View File

@ -0,0 +1,25 @@
import 'dart:convert';
import 'dart:typed_data';
import '../entities/local_account.dart';
import '../veilid_support/veilid_support.dart';
Future<Uint8List> encryptSecretToBytes(
{required SecretKey secret,
required CryptoKind cryptoKind,
EncryptionKeyType encryptionKeyType = EncryptionKeyType.none,
String encryptionKey = ''}) async {
final veilid = await eventualVeilid.future;
late final Uint8List identitySecretBytes;
switch (encryptionKeyType) {
case EncryptionKeyType.none:
identitySecretBytes = secret.decode();
case EncryptionKeyType.pin:
case EncryptionKeyType.password:
final cs = await veilid.getCryptoSystem(cryptoKind);
identitySecretBytes =
await cs.encryptNoAuthWithPassword(secret.decode(), encryptionKey);
}
return identitySecretBytes;
}

View File

@ -2,59 +2,13 @@
import 'dart:convert';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_chat_ui/flutter_chat_ui.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../entities/preferences.dart';
import 'radix_generator.dart';
@immutable
class ExtendedColorScheme extends ColorScheme {
const ExtendedColorScheme({
required this.scaleScheme,
required super.brightness,
required super.primary,
required super.onPrimary,
super.primaryContainer,
super.onPrimaryContainer,
required super.secondary,
required super.onSecondary,
super.secondaryContainer,
super.onSecondaryContainer,
super.tertiary,
super.onTertiary,
super.tertiaryContainer,
super.onTertiaryContainer,
required super.error,
required super.onError,
super.errorContainer,
super.onErrorContainer,
required super.background,
required super.onBackground,
required super.surface,
required super.onSurface,
super.surfaceVariant,
super.onSurfaceVariant,
super.outline,
super.outlineVariant,
super.shadow,
super.scrim,
super.inverseSurface,
super.onInverseSurface,
super.inversePrimary,
super.surfaceTint,
});
final ScaleScheme scaleScheme;
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(DiagnosticsProperty<ScaleScheme>('scales', scaleScheme));
}
}
class ScaleColor {
ScaleColor({
required this.appBackground,
@ -83,9 +37,69 @@ class ScaleColor {
Color hoverBackground;
Color subtleText;
Color text;
ScaleColor copyWith(
{Color? appBackground,
Color? subtleBackground,
Color? elementBackground,
Color? hoverElementBackground,
Color? activedElementBackground,
Color? subtleBorder,
Color? border,
Color? hoverBorder,
Color? background,
Color? hoverBackground,
Color? subtleText,
Color? text}) =>
ScaleColor(
appBackground: appBackground ?? this.appBackground,
subtleBackground: subtleBackground ?? this.subtleBackground,
elementBackground: elementBackground ?? this.elementBackground,
hoverElementBackground:
hoverElementBackground ?? this.hoverElementBackground,
activedElementBackground:
activedElementBackground ?? this.activedElementBackground,
subtleBorder: subtleBorder ?? this.subtleBorder,
border: border ?? this.border,
hoverBorder: hoverBorder ?? this.hoverBorder,
background: background ?? this.background,
hoverBackground: hoverBackground ?? this.hoverBackground,
subtleText: subtleText ?? this.subtleText,
text: text ?? this.text,
);
// ignore: prefer_constructors_over_static_methods
static ScaleColor lerp(ScaleColor a, ScaleColor b, double t) => ScaleColor(
appBackground: Color.lerp(a.appBackground, b.appBackground, t) ??
const Color(0x00000000),
subtleBackground:
Color.lerp(a.subtleBackground, b.subtleBackground, t) ??
const Color(0x00000000),
elementBackground:
Color.lerp(a.elementBackground, b.elementBackground, t) ??
const Color(0x00000000),
hoverElementBackground:
Color.lerp(a.hoverElementBackground, b.hoverElementBackground, t) ??
const Color(0x00000000),
activedElementBackground: Color.lerp(
a.activedElementBackground, b.activedElementBackground, t) ??
const Color(0x00000000),
subtleBorder: Color.lerp(a.subtleBorder, b.subtleBorder, t) ??
const Color(0x00000000),
border: Color.lerp(a.border, b.border, t) ?? const Color(0x00000000),
hoverBorder: Color.lerp(a.hoverBorder, b.hoverBorder, t) ??
const Color(0x00000000),
background: Color.lerp(a.background, b.background, t) ??
const Color(0x00000000),
hoverBackground: Color.lerp(a.hoverBackground, b.hoverBackground, t) ??
const Color(0x00000000),
subtleText: Color.lerp(a.subtleText, b.subtleText, t) ??
const Color(0x00000000),
text: Color.lerp(a.text, b.text, t) ?? const Color(0x00000000),
);
}
class ScaleScheme {
class ScaleScheme extends ThemeExtension<ScaleScheme> {
ScaleScheme(
{required this.primaryScale,
required this.primaryAlphaScale,
@ -94,15 +108,66 @@ class ScaleScheme {
required this.grayScale,
required this.errorScale});
ScaleColor primaryScale;
ScaleColor primaryAlphaScale;
ScaleColor secondaryScale;
ScaleColor tertiaryScale;
ScaleColor grayScale;
ScaleColor errorScale;
final ScaleColor primaryScale;
final ScaleColor primaryAlphaScale;
final ScaleColor secondaryScale;
final ScaleColor tertiaryScale;
final ScaleColor grayScale;
final ScaleColor errorScale;
static ScaleScheme of(BuildContext context) =>
(Theme.of(context).colorScheme as ExtendedColorScheme).scaleScheme;
@override
ScaleScheme copyWith(
{ScaleColor? primaryScale,
ScaleColor? primaryAlphaScale,
ScaleColor? secondaryScale,
ScaleColor? tertiaryScale,
ScaleColor? grayScale,
ScaleColor? errorScale}) =>
ScaleScheme(
primaryScale: primaryScale ?? this.primaryScale,
primaryAlphaScale: primaryAlphaScale ?? this.primaryAlphaScale,
secondaryScale: secondaryScale ?? this.secondaryScale,
tertiaryScale: tertiaryScale ?? this.tertiaryScale,
grayScale: grayScale ?? this.grayScale,
errorScale: errorScale ?? this.errorScale,
);
@override
ScaleScheme lerp(ScaleScheme? other, double t) {
if (other is! ScaleScheme) {
return this;
}
return ScaleScheme(
primaryScale: ScaleColor.lerp(primaryScale, other.primaryScale, t),
primaryAlphaScale:
ScaleColor.lerp(primaryAlphaScale, other.primaryAlphaScale, t),
secondaryScale: ScaleColor.lerp(secondaryScale, other.secondaryScale, t),
tertiaryScale: ScaleColor.lerp(tertiaryScale, other.tertiaryScale, t),
grayScale: ScaleColor.lerp(grayScale, other.grayScale, t),
errorScale: ScaleColor.lerp(errorScale, other.errorScale, t),
);
}
ChatTheme toChatTheme() => DefaultChatTheme(
primaryColor: primaryScale.background,
secondaryColor: secondaryScale.background,
backgroundColor: grayScale.subtleBackground,
inputBackgroundColor: primaryScale.appBackground,
inputBorderRadius: BorderRadius.zero,
inputTextDecoration: InputDecoration(
border: OutlineInputBorder(
borderSide: BorderSide(color: primaryScale.subtleBorder),
borderRadius: const BorderRadius.all(Radius.circular(16))),
),
inputContainerDecoration: BoxDecoration(color: grayScale.appBackground),
inputPadding: EdgeInsets.all(5),
inputTextStyle: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
height: 1,
),
attachmentButtonIcon: Icon(Icons.attach_file),
);
}
////////////////////////////////////////////////////////////////////////

View File

@ -5,5 +5,6 @@ export 'phono_byte.dart';
export 'protobuf_tools.dart';
export 'radix_generator.dart';
export 'responsive.dart';
export 'secret_crypto.dart';
export 'theme_service.dart';
export 'widget_helpers.dart';

View File

@ -1,8 +1,11 @@
import 'package:blurry_modal_progress_hud/blurry_modal_progress_hud.dart';
import 'package:flutter/material.dart';
import 'package:flutter_spinkit/flutter_spinkit.dart';
import 'package:motion_toast/motion_toast.dart';
import 'package:quickalert/quickalert.dart';
import 'theme_service.dart';
extension BorderExt on Widget {
DecoratedBox debugBorder() => DecoratedBox(
decoration: BoxDecoration(border: Border.all(color: Colors.redAccent)),
@ -10,19 +13,27 @@ extension BorderExt on Widget {
}
extension ModalProgressExt on Widget {
BlurryModalProgressHUD withModalHUD(BuildContext context, bool isLoading) =>
BlurryModalProgressHUD(
inAsyncCall: isLoading,
blurEffectIntensity: 4,
progressIndicator: buildProgressIndicator(context),
color: Theme.of(context).shadowColor,
child: this);
BlurryModalProgressHUD withModalHUD(BuildContext context, bool isLoading) {
final theme = Theme.of(context);
final scale = theme.extension<ScaleScheme>()!;
return BlurryModalProgressHUD(
inAsyncCall: isLoading,
blurEffectIntensity: 4,
progressIndicator: buildProgressIndicator(context),
color: scale.tertiaryScale.appBackground.withAlpha(64),
child: this);
}
}
Widget buildProgressIndicator(BuildContext context) => SpinKitFoldingCube(
color: Theme.of(context).highlightColor,
size: 90,
);
Widget buildProgressIndicator(BuildContext context) {
final theme = Theme.of(context);
final scale = theme.extension<ScaleScheme>()!;
return SpinKitFoldingCube(
color: scale.tertiaryScale.background,
size: 80,
);
}
Widget waitingPage(BuildContext context) => ColoredBox(
color: Theme.of(context).scaffoldBackgroundColor,
@ -40,3 +51,10 @@ Future<void> showErrorModal(
//textColor: Colors.white,
);
}
void showErrorToast(BuildContext context, String message) {
MotionToast.error(
title: Text("Error"),
description: Text(message),
).show(context);
}

View File

@ -19,7 +19,7 @@ class DHTRecord {
_recordDescriptor = recordDescriptor,
_defaultSubkey = defaultSubkey,
_writer = writer,
_open = false,
_open = true,
_valid = true,
_subkeySeqCache = {};
final VeilidRoutingContext _routingContext;

View File

@ -36,28 +36,12 @@ class DHTRecordCryptoPrivate implements DHTRecordCrypto {
}
@override
FutureOr<Uint8List> encrypt(Uint8List data, int subkey) async {
// generate nonce
final nonce = await _cryptoSystem.randomNonce();
// crypt and append nonce
final b = BytesBuilder()
..add(await _cryptoSystem.cryptNoAuth(data, nonce, _secretKey))
..add(nonce.decode());
return b.toBytes();
}
FutureOr<Uint8List> encrypt(Uint8List data, int subkey) =>
_cryptoSystem.encryptNoAuthWithNonce(data, _secretKey);
@override
FutureOr<Uint8List> decrypt(Uint8List data, int subkey) async {
// split off nonce from end
if (data.length <= Nonce.decodedLength()) {
throw const FormatException('not enough data to decrypt');
}
final nonce =
Nonce.fromBytes(data.sublist(data.length - Nonce.decodedLength()));
final encryptedData = data.sublist(0, data.length - Nonce.decodedLength());
// decrypt
return await _cryptoSystem.cryptNoAuth(encryptedData, nonce, _secretKey);
}
FutureOr<Uint8List> decrypt(Uint8List data, int subkey) =>
_cryptoSystem.decryptNoAuthWithNonce(data, _secretKey);
}
////////////////////////////////////

View File

@ -10,8 +10,10 @@ part 'dht_record_pool.g.dart';
@freezed
class DHTRecordPoolAllocations with _$DHTRecordPoolAllocations {
const factory DHTRecordPoolAllocations({
required IMap<TypedKey, ISet<TypedKey>> childrenByParent,
required IMap<TypedKey, TypedKey> parentByChild,
required IMap<String, ISet<TypedKey>>
childrenByParent, // String key due to IMap<> json unsupported in key
required IMap<String, TypedKey>
parentByChild, // String key due to IMap<> json unsupported in key
}) = _DHTRecordPoolAllocations;
factory DHTRecordPoolAllocations.fromJson(dynamic json) =>
@ -100,13 +102,14 @@ class DHTRecordPool with AsyncTableDBBacked<DHTRecordPoolAllocations> {
final nextDep = currentDeps.removeLast();
// Remove this child from its parent
_removeDependency(nextDep);
await _removeDependency(nextDep);
// Ensure all records are closed before delete
assert(!_opened.containsKey(nextDep), 'should not delete opened record');
allDeps.add(nextDep);
final childDeps = _state.childrenByParent[nextDep]?.toList() ?? [];
final childDeps =
_state.childrenByParent[nextDep.toJson()]?.toList() ?? [];
currentDeps.addAll(childDeps);
}
@ -118,41 +121,45 @@ class DHTRecordPool with AsyncTableDBBacked<DHTRecordPoolAllocations> {
await Future.wait(allFutures);
}
void _addDependency(TypedKey parent, TypedKey child) {
Future<void> _addDependency(TypedKey parent, TypedKey child) async {
final childrenOfParent =
_state.childrenByParent[parent] ?? ISet<TypedKey>();
_state.childrenByParent[parent.toJson()] ?? ISet<TypedKey>();
if (childrenOfParent.contains(child)) {
throw StateError('Dependency added twice: $parent -> $child');
// Dependency already added (consecutive opens, etc)
return;
}
if (_state.parentByChild.containsKey(child)) {
if (_state.parentByChild.containsKey(child.toJson())) {
throw StateError('Child has two parents: $child <- $parent');
}
if (_state.childrenByParent.containsKey(child)) {
if (_state.childrenByParent.containsKey(child.toJson())) {
// dependencies should be opened after their parents
throw StateError('Child is not a leaf: $child');
}
_state = _state.copyWith(
childrenByParent:
_state.childrenByParent.add(parent, childrenOfParent.add(child)),
parentByChild: _state.parentByChild.add(child, parent));
_state = await store(_state.copyWith(
childrenByParent: _state.childrenByParent
.add(parent.toJson(), childrenOfParent.add(child)),
parentByChild: _state.parentByChild.add(child.toJson(), parent)));
}
void _removeDependency(TypedKey child) {
final parent = _state.parentByChild[child];
Future<void> _removeDependency(TypedKey child) async {
final parent = _state.parentByChild[child.toJson()];
if (parent == null) {
return;
}
final children = _state.childrenByParent[parent]!.remove(child);
final children = _state.childrenByParent[parent.toJson()]!.remove(child);
late final DHTRecordPoolAllocations newState;
if (children.isEmpty) {
_state = _state.copyWith(
childrenByParent: _state.childrenByParent.remove(parent),
parentByChild: _state.parentByChild.remove(child));
newState = _state.copyWith(
childrenByParent: _state.childrenByParent.remove(parent.toJson()),
parentByChild: _state.parentByChild.remove(child.toJson()));
} else {
_state = _state.copyWith(
childrenByParent: _state.childrenByParent.add(parent, children),
parentByChild: _state.parentByChild.remove(child));
newState = _state.copyWith(
childrenByParent:
_state.childrenByParent.add(parent.toJson(), children),
parentByChild: _state.parentByChild.remove(child.toJson()));
}
_state = await store(newState);
}
///////////////////////////////////////////////////////////////////////
@ -177,7 +184,7 @@ class DHTRecordPool with AsyncTableDBBacked<DHTRecordPoolAllocations> {
recordDescriptor.ownerTypedKeyPair()!));
if (parent != null) {
_addDependency(parent, rec.key);
await _addDependency(parent, rec.key);
}
_recordOpened(rec);
@ -192,7 +199,7 @@ class DHTRecordPool with AsyncTableDBBacked<DHTRecordPoolAllocations> {
DHTRecordCrypto? crypto}) async {
// If we are opening a key that already exists
// make sure we are using the same parent if one was specified
final existingParent = _state.parentByChild[recordKey];
final existingParent = _state.parentByChild[recordKey.toJson()];
assert(existingParent == parent, 'wrong parent for opened key');
// Open from the veilid api
@ -206,7 +213,7 @@ class DHTRecordPool with AsyncTableDBBacked<DHTRecordPoolAllocations> {
// Register the dependency if specified
if (parent != null) {
_addDependency(parent, rec.key);
await _addDependency(parent, rec.key);
}
_recordOpened(rec);
@ -224,7 +231,7 @@ class DHTRecordPool with AsyncTableDBBacked<DHTRecordPoolAllocations> {
}) async {
// If we are opening a key that already exists
// make sure we are using the same parent if one was specified
final existingParent = _state.parentByChild[recordKey];
final existingParent = _state.parentByChild[recordKey.toJson()];
assert(existingParent == parent, 'wrong parent for opened key');
// Open from the veilid api
@ -241,7 +248,7 @@ class DHTRecordPool with AsyncTableDBBacked<DHTRecordPoolAllocations> {
// Register the dependency if specified
if (parent != null) {
_addDependency(parent, rec.key);
await _addDependency(parent, rec.key);
}
_recordOpened(rec);

View File

@ -21,10 +21,10 @@ DHTRecordPoolAllocations _$DHTRecordPoolAllocationsFromJson(
/// @nodoc
mixin _$DHTRecordPoolAllocations {
IMap<Typed<FixedEncodedString43>, ISet<Typed<FixedEncodedString43>>>
get childrenByParent => throw _privateConstructorUsedError;
IMap<Typed<FixedEncodedString43>, Typed<FixedEncodedString43>>
get parentByChild => throw _privateConstructorUsedError;
IMap<String, ISet<Typed<FixedEncodedString43>>> get childrenByParent =>
throw _privateConstructorUsedError; // String key due to IMap<> json unsupported in key
IMap<String, Typed<FixedEncodedString43>> get parentByChild =>
throw _privateConstructorUsedError;
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
@JsonKey(ignore: true)
@ -39,10 +39,8 @@ abstract class $DHTRecordPoolAllocationsCopyWith<$Res> {
_$DHTRecordPoolAllocationsCopyWithImpl<$Res, DHTRecordPoolAllocations>;
@useResult
$Res call(
{IMap<Typed<FixedEncodedString43>, ISet<Typed<FixedEncodedString43>>>
childrenByParent,
IMap<Typed<FixedEncodedString43>, Typed<FixedEncodedString43>>
parentByChild});
{IMap<String, ISet<Typed<FixedEncodedString43>>> childrenByParent,
IMap<String, Typed<FixedEncodedString43>> parentByChild});
}
/// @nodoc
@ -66,12 +64,11 @@ class _$DHTRecordPoolAllocationsCopyWithImpl<$Res,
childrenByParent: null == childrenByParent
? _value.childrenByParent
: childrenByParent // ignore: cast_nullable_to_non_nullable
as IMap<Typed<FixedEncodedString43>,
ISet<Typed<FixedEncodedString43>>>,
as IMap<String, ISet<Typed<FixedEncodedString43>>>,
parentByChild: null == parentByChild
? _value.parentByChild
: parentByChild // ignore: cast_nullable_to_non_nullable
as IMap<Typed<FixedEncodedString43>, Typed<FixedEncodedString43>>,
as IMap<String, Typed<FixedEncodedString43>>,
) as $Val);
}
}
@ -86,10 +83,8 @@ abstract class _$$_DHTRecordPoolAllocationsCopyWith<$Res>
@override
@useResult
$Res call(
{IMap<Typed<FixedEncodedString43>, ISet<Typed<FixedEncodedString43>>>
childrenByParent,
IMap<Typed<FixedEncodedString43>, Typed<FixedEncodedString43>>
parentByChild});
{IMap<String, ISet<Typed<FixedEncodedString43>>> childrenByParent,
IMap<String, Typed<FixedEncodedString43>> parentByChild});
}
/// @nodoc
@ -111,12 +106,11 @@ class __$$_DHTRecordPoolAllocationsCopyWithImpl<$Res>
childrenByParent: null == childrenByParent
? _value.childrenByParent
: childrenByParent // ignore: cast_nullable_to_non_nullable
as IMap<Typed<FixedEncodedString43>,
ISet<Typed<FixedEncodedString43>>>,
as IMap<String, ISet<Typed<FixedEncodedString43>>>,
parentByChild: null == parentByChild
? _value.parentByChild
: parentByChild // ignore: cast_nullable_to_non_nullable
as IMap<Typed<FixedEncodedString43>, Typed<FixedEncodedString43>>,
as IMap<String, Typed<FixedEncodedString43>>,
));
}
}
@ -131,11 +125,10 @@ class _$_DHTRecordPoolAllocations implements _DHTRecordPoolAllocations {
_$$_DHTRecordPoolAllocationsFromJson(json);
@override
final IMap<Typed<FixedEncodedString43>, ISet<Typed<FixedEncodedString43>>>
childrenByParent;
final IMap<String, ISet<Typed<FixedEncodedString43>>> childrenByParent;
// String key due to IMap<> json unsupported in key
@override
final IMap<Typed<FixedEncodedString43>, Typed<FixedEncodedString43>>
parentByChild;
final IMap<String, Typed<FixedEncodedString43>> parentByChild;
@override
String toString() {
@ -174,22 +167,18 @@ class _$_DHTRecordPoolAllocations implements _DHTRecordPoolAllocations {
abstract class _DHTRecordPoolAllocations implements DHTRecordPoolAllocations {
const factory _DHTRecordPoolAllocations(
{required final IMap<Typed<FixedEncodedString43>,
ISet<Typed<FixedEncodedString43>>>
{required final IMap<String, ISet<Typed<FixedEncodedString43>>>
childrenByParent,
required final IMap<Typed<FixedEncodedString43>,
Typed<FixedEncodedString43>>
required final IMap<String, Typed<FixedEncodedString43>>
parentByChild}) = _$_DHTRecordPoolAllocations;
factory _DHTRecordPoolAllocations.fromJson(Map<String, dynamic> json) =
_$_DHTRecordPoolAllocations.fromJson;
@override
IMap<Typed<FixedEncodedString43>, ISet<Typed<FixedEncodedString43>>>
get childrenByParent;
@override
IMap<Typed<FixedEncodedString43>, Typed<FixedEncodedString43>>
get parentByChild;
IMap<String, ISet<Typed<FixedEncodedString43>>> get childrenByParent;
@override // String key due to IMap<> json unsupported in key
IMap<String, Typed<FixedEncodedString43>> get parentByChild;
@override
@JsonKey(ignore: true)
_$$_DHTRecordPoolAllocationsCopyWith<_$_DHTRecordPoolAllocations>

View File

@ -9,16 +9,15 @@ part of 'dht_record_pool.dart';
_$_DHTRecordPoolAllocations _$$_DHTRecordPoolAllocationsFromJson(
Map<String, dynamic> json) =>
_$_DHTRecordPoolAllocations(
childrenByParent: IMap<Typed<FixedEncodedString43>,
ISet<Typed<FixedEncodedString43>>>.fromJson(
json['children_by_parent'] as Map<String, dynamic>,
(value) => Typed<FixedEncodedString43>.fromJson(value),
(value) => ISet<Typed<FixedEncodedString43>>.fromJson(
value, (value) => Typed<FixedEncodedString43>.fromJson(value))),
parentByChild: IMap<Typed<FixedEncodedString43>,
Typed<FixedEncodedString43>>.fromJson(
childrenByParent:
IMap<String, ISet<Typed<FixedEncodedString43>>>.fromJson(
json['children_by_parent'] as Map<String, dynamic>,
(value) => value as String,
(value) => ISet<Typed<FixedEncodedString43>>.fromJson(value,
(value) => Typed<FixedEncodedString43>.fromJson(value))),
parentByChild: IMap<String, Typed<FixedEncodedString43>>.fromJson(
json['parent_by_child'] as Map<String, dynamic>,
(value) => Typed<FixedEncodedString43>.fromJson(value),
(value) => value as String,
(value) => Typed<FixedEncodedString43>.fromJson(value)),
);
@ -26,13 +25,13 @@ Map<String, dynamic> _$$_DHTRecordPoolAllocationsToJson(
_$_DHTRecordPoolAllocations instance) =>
<String, dynamic>{
'children_by_parent': instance.childrenByParent.toJson(
(value) => value.toJson(),
(value) => value,
(value) => value.toJson(
(value) => value.toJson(),
),
),
'parent_by_child': instance.parentByChild.toJson(
(value) => value.toJson(),
(value) => value,
(value) => value.toJson(),
),
};

View File

@ -26,50 +26,52 @@ class IdentityMasterWithSecrets {
return (await pool.create(crypto: const DHTRecordCryptoPublic()))
.deleteScope((masterRec) async {
// Identity record is private
final identityRec = await pool.create(parent: masterRec.key);
// Make IdentityMaster
final masterRecordKey = masterRec.key;
final masterOwner = masterRec.ownerKeyPair!;
final masterSigBuf = BytesBuilder()
..add(masterRecordKey.decode())
..add(masterOwner.key.decode());
return (await pool.create(parent: masterRec.key))
.scope((identityRec) async {
// Make IdentityMaster
final masterRecordKey = masterRec.key;
final masterOwner = masterRec.ownerKeyPair!;
final masterSigBuf = BytesBuilder()
..add(masterRecordKey.decode())
..add(masterOwner.key.decode());
final identityRecordKey = identityRec.key;
final identityOwner = identityRec.ownerKeyPair!;
final identitySigBuf = BytesBuilder()
..add(identityRecordKey.decode())
..add(identityOwner.key.decode());
final identityRecordKey = identityRec.key;
final identityOwner = identityRec.ownerKeyPair!;
final identitySigBuf = BytesBuilder()
..add(identityRecordKey.decode())
..add(identityOwner.key.decode());
assert(masterRecordKey.kind == identityRecordKey.kind,
'new master and identity should have same cryptosystem');
final crypto = await pool.veilid.getCryptoSystem(masterRecordKey.kind);
assert(masterRecordKey.kind == identityRecordKey.kind,
'new master and identity should have same cryptosystem');
final crypto = await pool.veilid.getCryptoSystem(masterRecordKey.kind);
final identitySignature =
await crypto.signWithKeyPair(masterOwner, identitySigBuf.toBytes());
final masterSignature =
await crypto.signWithKeyPair(identityOwner, masterSigBuf.toBytes());
final identitySignature =
await crypto.signWithKeyPair(masterOwner, identitySigBuf.toBytes());
final masterSignature =
await crypto.signWithKeyPair(identityOwner, masterSigBuf.toBytes());
final identityMaster = IdentityMaster(
identityRecordKey: identityRecordKey,
identityPublicKey: identityOwner.key,
masterRecordKey: masterRecordKey,
masterPublicKey: masterOwner.key,
identitySignature: identitySignature,
masterSignature: masterSignature);
final identityMaster = IdentityMaster(
identityRecordKey: identityRecordKey,
identityPublicKey: identityOwner.key,
masterRecordKey: masterRecordKey,
masterPublicKey: masterOwner.key,
identitySignature: identitySignature,
masterSignature: masterSignature);
// Write identity master to master dht key
await masterRec.eventualWriteJson(identityMaster);
// Write identity master to master dht key
await masterRec.eventualWriteJson(identityMaster);
// Make empty identity
const identity = Identity(accountRecords: IMapConst({}));
// Make empty identity
const identity = Identity(accountRecords: IMapConst({}));
// Write empty identity to identity dht key
await identityRec.eventualWriteJson(identity);
// Write empty identity to identity dht key
await identityRec.eventualWriteJson(identity);
return IdentityMasterWithSecrets._(
identityMaster: identityMaster,
masterSecret: masterOwner.secret,
identitySecret: identityOwner.secret);
return IdentityMasterWithSecrets._(
identityMaster: identityMaster,
masterSecret: masterOwner.secret,
identitySecret: identityOwner.secret);
});
});
}

View File

@ -47,9 +47,10 @@ abstract mixin class AsyncTableDBBacked<T> {
}
/// Store things to storage
Future<void> store(T obj) async {
Future<T> store(T obj) async {
await tableScope(tableName(), (tdb) async {
await tdb.storeStringJson(0, tableKeyName(), valueToJson(obj));
});
return obj;
}
}

View File

@ -7,6 +7,7 @@
#include "generated_plugin_registrant.h"
#include <screen_retriever/screen_retriever_plugin.h>
#include <smart_auth/smart_auth_plugin.h>
#include <url_launcher_linux/url_launcher_plugin.h>
#include <veilid/veilid_plugin.h>
#include <window_manager/window_manager_plugin.h>
@ -15,6 +16,9 @@ void fl_register_plugins(FlPluginRegistry* registry) {
g_autoptr(FlPluginRegistrar) screen_retriever_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "ScreenRetrieverPlugin");
screen_retriever_plugin_register_with_registrar(screen_retriever_registrar);
g_autoptr(FlPluginRegistrar) smart_auth_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "SmartAuthPlugin");
smart_auth_plugin_register_with_registrar(smart_auth_registrar);
g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin");
url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar);

View File

@ -4,6 +4,7 @@
list(APPEND FLUTTER_PLUGIN_LIST
screen_retriever
smart_auth
url_launcher_linux
veilid
window_manager

View File

@ -9,6 +9,7 @@ import path_provider_foundation
import screen_retriever
import share_plus
import shared_preferences_foundation
import smart_auth
import sqflite
import url_launcher_macos
import veilid
@ -19,6 +20,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
ScreenRetrieverPlugin.register(with: registry.registrar(forPlugin: "ScreenRetrieverPlugin"))
SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin"))
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
SmartAuthPlugin.register(with: registry.registrar(forPlugin: "SmartAuthPlugin"))
SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin"))
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
VeilidPlugin.register(with: registry.registrar(forPlugin: "VeilidPlugin"))

View File

@ -8,9 +8,13 @@ PODS:
- FlutterMacOS
- screen_retriever (0.0.1):
- FlutterMacOS
- share_plus (0.0.1):
- FlutterMacOS
- shared_preferences_foundation (0.0.1):
- Flutter
- FlutterMacOS
- smart_auth (0.0.1):
- FlutterMacOS
- sqflite (0.0.2):
- FlutterMacOS
- FMDB (>= 2.7.5)
@ -25,7 +29,9 @@ DEPENDENCIES:
- FlutterMacOS (from `Flutter/ephemeral`)
- path_provider_foundation (from `Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin`)
- screen_retriever (from `Flutter/ephemeral/.symlinks/plugins/screen_retriever/macos`)
- share_plus (from `Flutter/ephemeral/.symlinks/plugins/share_plus/macos`)
- shared_preferences_foundation (from `Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin`)
- smart_auth (from `Flutter/ephemeral/.symlinks/plugins/smart_auth/macos`)
- sqflite (from `Flutter/ephemeral/.symlinks/plugins/sqflite/macos`)
- url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`)
- veilid (from `Flutter/ephemeral/.symlinks/plugins/veilid/macos`)
@ -42,8 +48,12 @@ EXTERNAL SOURCES:
:path: Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin
screen_retriever:
:path: Flutter/ephemeral/.symlinks/plugins/screen_retriever/macos
share_plus:
:path: Flutter/ephemeral/.symlinks/plugins/share_plus/macos
shared_preferences_foundation:
:path: Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin
smart_auth:
:path: Flutter/ephemeral/.symlinks/plugins/smart_auth/macos
sqflite:
:path: Flutter/ephemeral/.symlinks/plugins/sqflite/macos
url_launcher_macos:
@ -58,7 +68,9 @@ SPEC CHECKSUMS:
FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a
path_provider_foundation: 29f094ae23ebbca9d3d0cec13889cd9060c0e943
screen_retriever: 59634572a57080243dd1bf715e55b6c54f241a38
share_plus: 76dd39142738f7a68dd57b05093b5e8193f220f7
shared_preferences_foundation: 5b919d13b803cadd15ed2dc053125c68730e5126
smart_auth: b38e3ab4bfe089eacb1e233aca1a2340f96c28e9
sqflite: a5789cceda41d54d23f31d6de539d65bb14100ea
url_launcher_macos: d2691c7dd33ed713bf3544850a623080ec693d95
veilid: a54f57b7bcf0e4e072fe99272d76ca126b2026d0

View File

@ -757,6 +757,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.0.4"
motion_toast:
dependency: "direct main"
description:
name: motion_toast
sha256: f27cfcd39c6a0c433670fb20e4add55c42f925a5382b25f58e917c054d47a624
url: "https://pub.dev"
source: hosted
version: "2.7.8"
octo_image:
dependency: transitive
description:
@ -853,6 +861,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.14.0"
pinput:
dependency: "direct main"
description:
name: pinput
sha256: "543da5bfdefd9e06914a12100f8c9156f84cef3efc14bca507c49e966c5b813b"
url: "https://pub.dev"
source: hosted
version: "2.3.0"
platform:
dependency: transitive
description:
@ -1090,6 +1106,14 @@ packages:
description: flutter
source: sdk
version: "0.0.99"
smart_auth:
dependency: transitive
description:
name: smart_auth
sha256: a25229b38c02f733d0a4e98d941b42bed91a976cb589e934895e60ccfa674cf6
url: "https://pub.dev"
source: hosted
version: "1.1.1"
source_gen:
dependency: transitive
description:
@ -1250,6 +1274,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.2.2"
universal_platform:
dependency: transitive
description:
name: universal_platform
sha256: d315be0f6641898b280ffa34e2ddb14f3d12b1a37882557869646e0cc363d0cc
url: "https://pub.dev"
source: hosted
version: "1.0.0+1"
url_launcher:
dependency: transitive
description:
@ -1360,7 +1392,7 @@ packages:
path: "../veilid/veilid-flutter"
relative: true
source: path
version: "0.1.6"
version: "0.1.7"
visibility_detector:
dependency: transitive
description:
@ -1427,4 +1459,4 @@ packages:
version: "3.1.2"
sdks:
dart: ">=3.0.5 <4.0.0"
flutter: ">=3.10.0"
flutter: ">=3.10.6"

View File

@ -40,8 +40,10 @@ dependencies:
intl: ^0.18.0
json_annotation: ^4.8.1
loggy: ^2.0.3
motion_toast: ^2.7.8
path: ^1.8.2
path_provider: ^2.0.11
pinput: ^2.3.0
protobuf: ^3.0.0
quickalert: ^1.0.1
radix_colors: ^1.0.4

View File

@ -8,6 +8,7 @@
#include <screen_retriever/screen_retriever_plugin.h>
#include <share_plus/share_plus_windows_plugin_c_api.h>
#include <smart_auth/smart_auth_plugin.h>
#include <url_launcher_windows/url_launcher_windows.h>
#include <veilid/veilid_plugin.h>
#include <window_manager/window_manager_plugin.h>
@ -17,6 +18,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) {
registry->GetRegistrarForPlugin("ScreenRetrieverPlugin"));
SharePlusWindowsPluginCApiRegisterWithRegistrar(
registry->GetRegistrarForPlugin("SharePlusWindowsPluginCApi"));
SmartAuthPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("SmartAuthPlugin"));
UrlLauncherWindowsRegisterWithRegistrar(
registry->GetRegistrarForPlugin("UrlLauncherWindows"));
VeilidPluginRegisterWithRegistrar(

View File

@ -5,6 +5,7 @@
list(APPEND FLUTTER_PLUGIN_LIST
screen_retriever
share_plus
smart_auth
url_launcher_windows
veilid
window_manager