diff --git a/lib/components/contact_invitation_display.dart b/lib/components/contact_invitation_display.dart index 65cb0c1..f23bece 100644 --- a/lib/components/contact_invitation_display.dart +++ b/lib/components/contact_invitation_display.dart @@ -1,7 +1,5 @@ import 'dart:async'; -import 'package:archive/archive.dart'; -import 'package:awesome_extensions/awesome_extensions.dart'; import 'package:basic_utils/basic_utils.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; @@ -74,7 +72,7 @@ class ContactInvitationDisplayDialogState // ignore: prefer_expression_function_bodies Widget build(BuildContext context) { final theme = Theme.of(context); - final scale = theme.extension()!; + //final scale = theme.extension()!; final textTheme = theme.textTheme; final signedContactInvitationBytesV = ref.watch(_generateFutureProvider); diff --git a/lib/components/contact_invitation_item_widget.dart b/lib/components/contact_invitation_item_widget.dart index 314e039..92e472d 100644 --- a/lib/components/contact_invitation_item_widget.dart +++ b/lib/components/contact_invitation_item_widget.dart @@ -3,10 +3,9 @@ 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 '../providers/contact_invite.dart'; import '../tools/tools.dart'; import 'contact_invitation_display.dart'; @@ -27,7 +26,7 @@ class ContactInvitationItemWidget extends ConsumerWidget { // ignore: prefer_expression_function_bodies Widget build(BuildContext context, WidgetRef ref) { final theme = Theme.of(context); - final textTheme = theme.textTheme; + //final textTheme = theme.textTheme; final scale = theme.extension()!; return Container( diff --git a/lib/components/contact_item_widget.dart b/lib/components/contact_item_widget.dart index bbbcaf4..363c4e4 100644 --- a/lib/components/contact_item_widget.dart +++ b/lib/components/contact_item_widget.dart @@ -1,7 +1,12 @@ +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 '../../entities/proto.dart' as proto; +import '../providers/account.dart'; +import '../providers/contact.dart'; +import '../tools/theme_service.dart'; class ContactItemWidget extends ConsumerWidget { const ContactItemWidget({required this.contact, super.key}); @@ -11,65 +16,83 @@ class ContactItemWidget extends ConsumerWidget { @override // ignore: prefer_expression_function_bodies Widget build(BuildContext context, WidgetRef ref) { - return Slidable( - // Specify a key if the Slidable is dismissible. - key: ObjectKey(contact), - // The start action pane is the one at the left or the top side. - startActionPane: ActionPane( - // A motion is a widget used to control how the pane animates. - motion: const DrawerMotion(), + final theme = Theme.of(context); + final textTheme = theme.textTheme; + final scale = theme.extension()!; - // A pane can dismiss the Slidable. - //dismissible: DismissiblePane(onDismissed: () {}), - - // All actions are defined in the children parameter. - children: [ - // A SlidableAction can have an icon and/or a label. - SlidableAction( - onPressed: (context) => (), - backgroundColor: Color(0xFFFE4A49), - foregroundColor: Colors.white, - icon: Icons.delete, - label: 'Delete', + 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( + key: ObjectKey(contact), + endActionPane: ActionPane( + motion: const DrawerMotion(), + children: [ + SlidableAction( + onPressed: (context) async { + final activeAccountInfo = + await ref.read(fetchActiveAccountProvider.future); + if (activeAccountInfo != null) { + await deleteContact( + activeAccountInfo: activeAccountInfo, + contact: contact); + ref.invalidate(fetchContactListProvider); + } + }, + backgroundColor: scale.tertiaryScale.background, + foregroundColor: scale.tertiaryScale.text, + icon: Icons.delete, + label: translate('button.delete'), + padding: const EdgeInsets.all(2)), + // SlidableAction( + // onPressed: (context) => (), + // backgroundColor: scale.secondaryScale.background, + // foregroundColor: scale.secondaryScale.text, + // icon: Icons.edit, + // label: 'Edit', + // ), + ], ), - SlidableAction( - onPressed: (context) => (), - backgroundColor: Color(0xFF21B7CA), - foregroundColor: Colors.white, - icon: Icons.edit, - label: 'Edit', - ), - ], - ), - // The end action pane is the one at the right or the bottom side. - // endActionPane: ActionPane( - // motion: const DrawerMotion(), - // children: [ - // SlidableAction( - // // An action can be bigger than the others. - // flex: 2, - // onPressed: (context) => (), - // backgroundColor: Color(0xFF7BC043), - // foregroundColor: Colors.white, - // icon: Icons.archive, - // label: 'Archive', - // ), - // SlidableAction( - // onPressed: (context) => (), - // backgroundColor: Color(0xFF0392CF), - // foregroundColor: Colors.white, - // icon: Icons.save, - // label: 'Save', - // ), - // ], - // ), + // The child of the Slidable is what the user sees when the + // component is not dragged. + child: ListTile( + onTap: () async { + // final activeAccountInfo = + // await ref.read(fetchActiveAccountProvider.future); + // if (activeAccountInfo != null) { + // // ignore: use_build_context_synchronously + // if (!context.mounted) { + // return; + // } + // await showDialog( + // context: context, + // builder: (context) => ContactInvitationDisplayDialog( + // name: activeAccountInfo.localAccount.name, + // message: contactInvitationRecord.message, + // generator: Uint8List.fromList( + // contactInvitationRecord.invitation), + // )); + // } + }, + title: Text(contact.editedProfile.name), + subtitle: (contact.editedProfile.title.isNotEmpty) + ? Text(contact.editedProfile.title) + : null, + iconColor: scale.tertiaryScale.background, + textColor: scale.tertiaryScale.text, + //Text(Timestamp.fromInt64(contactInvitationRecord.expiration) / ), + leading: const Icon(Icons.person)))); + } - // The child of the Slidable is what the user sees when the - // component is not dragged. - child: ListTile( - title: Text(contact.editedProfile.name), - subtitle: Text(contact.editedProfile.title), - leading: Icon(Icons.person))); + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.add(DiagnosticsProperty('contact', contact)); } } diff --git a/lib/components/paste_invite_dialog.dart b/lib/components/paste_invite_dialog.dart index a673a2c..63eca05 100644 --- a/lib/components/paste_invite_dialog.dart +++ b/lib/components/paste_invite_dialog.dart @@ -1,5 +1,4 @@ import 'dart:async'; -import 'dart:typed_data'; import 'package:awesome_extensions/awesome_extensions.dart'; import 'package:flutter/foundation.dart'; @@ -9,12 +8,11 @@ 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 '../providers/contact_invite.dart'; import '../tools/tools.dart'; import '../veilid_support/veilid_support.dart'; -import 'contact_invitation_display.dart'; import 'enter_pin.dart'; import 'profile_widget.dart'; @@ -118,7 +116,7 @@ class PasteInviteDialogState extends ConsumerState { activeAccountInfo: activeAccountInfo, profile: acceptedContact.profile, remoteIdentity: acceptedContact.remoteIdentity, - remoteConversation: acceptedContact.remoteConversation, + remoteConversationKey: acceptedContact.remoteConversationKey, localConversation: acceptedContact.localConversation, ); ref diff --git a/lib/components/send_invite_dialog.dart b/lib/components/send_invite_dialog.dart index bf12fdb..35b85a0 100644 --- a/lib/components/send_invite_dialog.dart +++ b/lib/components/send_invite_dialog.dart @@ -1,5 +1,4 @@ import 'dart:async'; -import 'dart:typed_data'; import 'package:awesome_extensions/awesome_extensions.dart'; import 'package:flutter/foundation.dart'; @@ -7,11 +6,10 @@ 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'; import '../entities/local_account.dart'; import '../providers/account.dart'; -import '../providers/contact.dart'; +import '../providers/contact_invite.dart'; import '../tools/tools.dart'; import '../veilid_support/veilid_support.dart'; import 'contact_invitation_display.dart'; diff --git a/lib/entities/proto/veilidchat.pb.dart b/lib/entities/proto/veilidchat.pb.dart index cb258fc..fc3ca45 100644 --- a/lib/entities/proto/veilidchat.pb.dart +++ b/lib/entities/proto/veilidchat.pb.dart @@ -931,8 +931,8 @@ class Conversation extends $pb.GeneratedMessage { static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'Conversation', createEmptyInstance: create) ..aOM(1, _omitFieldNames ? '' : 'profile', subBuilder: Profile.create) - ..aOS(2, _omitFieldNames ? '' : 'identity') - ..aOM(3, _omitFieldNames ? '' : 'messages', subBuilder: DHTLog.create) + ..aOS(2, _omitFieldNames ? '' : 'identityMasterJson') + ..aOM(3, _omitFieldNames ? '' : 'messages', subBuilder: OwnedDHTRecordPointer.create) ..hasRequiredFields = false ; @@ -969,24 +969,24 @@ class Conversation extends $pb.GeneratedMessage { Profile ensureProfile() => $_ensure(0); @$pb.TagNumber(2) - $core.String get identity => $_getSZ(1); + $core.String get identityMasterJson => $_getSZ(1); @$pb.TagNumber(2) - set identity($core.String v) { $_setString(1, v); } + set identityMasterJson($core.String v) { $_setString(1, v); } @$pb.TagNumber(2) - $core.bool hasIdentity() => $_has(1); + $core.bool hasIdentityMasterJson() => $_has(1); @$pb.TagNumber(2) - void clearIdentity() => clearField(2); + void clearIdentityMasterJson() => clearField(2); @$pb.TagNumber(3) - DHTLog get messages => $_getN(2); + OwnedDHTRecordPointer get messages => $_getN(2); @$pb.TagNumber(3) - set messages(DHTLog v) { setField(3, v); } + set messages(OwnedDHTRecordPointer v) { setField(3, v); } @$pb.TagNumber(3) $core.bool hasMessages() => $_has(2); @$pb.TagNumber(3) void clearMessages() => clearField(3); @$pb.TagNumber(3) - DHTLog ensureMessages() => $_ensure(2); + OwnedDHTRecordPointer ensureMessages() => $_ensure(2); } class Contact extends $pb.GeneratedMessage { @@ -998,10 +998,11 @@ class Contact extends $pb.GeneratedMessage { static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'Contact', createEmptyInstance: create) ..aOM(1, _omitFieldNames ? '' : 'editedProfile', subBuilder: Profile.create) ..aOM(2, _omitFieldNames ? '' : 'remoteProfile', subBuilder: Profile.create) - ..aOS(3, _omitFieldNames ? '' : 'remoteIdentity') - ..aOM(4, _omitFieldNames ? '' : 'remoteConversationKey', subBuilder: TypedKey.create) - ..aOM(5, _omitFieldNames ? '' : 'localConversation', subBuilder: OwnedDHTRecordPointer.create) - ..aOB(6, _omitFieldNames ? '' : 'showAvailability') + ..aOS(3, _omitFieldNames ? '' : 'identityMasterJson') + ..aOM(4, _omitFieldNames ? '' : 'identityPublicKey', subBuilder: TypedKey.create) + ..aOM(5, _omitFieldNames ? '' : 'remoteConversationKey', subBuilder: TypedKey.create) + ..aOM(6, _omitFieldNames ? '' : 'localConversation', subBuilder: OwnedDHTRecordPointer.create) + ..aOB(7, _omitFieldNames ? '' : 'showAvailability') ..hasRequiredFields = false ; @@ -1049,44 +1050,55 @@ class Contact extends $pb.GeneratedMessage { Profile ensureRemoteProfile() => $_ensure(1); @$pb.TagNumber(3) - $core.String get remoteIdentity => $_getSZ(2); + $core.String get identityMasterJson => $_getSZ(2); @$pb.TagNumber(3) - set remoteIdentity($core.String v) { $_setString(2, v); } + set identityMasterJson($core.String v) { $_setString(2, v); } @$pb.TagNumber(3) - $core.bool hasRemoteIdentity() => $_has(2); + $core.bool hasIdentityMasterJson() => $_has(2); @$pb.TagNumber(3) - void clearRemoteIdentity() => clearField(3); + void clearIdentityMasterJson() => clearField(3); @$pb.TagNumber(4) - TypedKey get remoteConversationKey => $_getN(3); + TypedKey get identityPublicKey => $_getN(3); @$pb.TagNumber(4) - set remoteConversationKey(TypedKey v) { setField(4, v); } + set identityPublicKey(TypedKey v) { setField(4, v); } @$pb.TagNumber(4) - $core.bool hasRemoteConversationKey() => $_has(3); + $core.bool hasIdentityPublicKey() => $_has(3); @$pb.TagNumber(4) - void clearRemoteConversationKey() => clearField(4); + void clearIdentityPublicKey() => clearField(4); @$pb.TagNumber(4) - TypedKey ensureRemoteConversationKey() => $_ensure(3); + TypedKey ensureIdentityPublicKey() => $_ensure(3); @$pb.TagNumber(5) - OwnedDHTRecordPointer get localConversation => $_getN(4); + TypedKey get remoteConversationKey => $_getN(4); @$pb.TagNumber(5) - set localConversation(OwnedDHTRecordPointer v) { setField(5, v); } + set remoteConversationKey(TypedKey v) { setField(5, v); } @$pb.TagNumber(5) - $core.bool hasLocalConversation() => $_has(4); + $core.bool hasRemoteConversationKey() => $_has(4); @$pb.TagNumber(5) - void clearLocalConversation() => clearField(5); + void clearRemoteConversationKey() => clearField(5); @$pb.TagNumber(5) - OwnedDHTRecordPointer ensureLocalConversation() => $_ensure(4); + TypedKey ensureRemoteConversationKey() => $_ensure(4); @$pb.TagNumber(6) - $core.bool get showAvailability => $_getBF(5); + OwnedDHTRecordPointer get localConversation => $_getN(5); @$pb.TagNumber(6) - set showAvailability($core.bool v) { $_setBool(5, v); } + set localConversation(OwnedDHTRecordPointer v) { setField(6, v); } @$pb.TagNumber(6) - $core.bool hasShowAvailability() => $_has(5); + $core.bool hasLocalConversation() => $_has(5); @$pb.TagNumber(6) - void clearShowAvailability() => clearField(6); + void clearLocalConversation() => clearField(6); + @$pb.TagNumber(6) + OwnedDHTRecordPointer ensureLocalConversation() => $_ensure(5); + + @$pb.TagNumber(7) + $core.bool get showAvailability => $_getBF(6); + @$pb.TagNumber(7) + set showAvailability($core.bool v) { $_setBool(6, v); } + @$pb.TagNumber(7) + $core.bool hasShowAvailability() => $_has(6); + @$pb.TagNumber(7) + void clearShowAvailability() => clearField(7); } class Profile extends $pb.GeneratedMessage { @@ -1229,6 +1241,60 @@ class OwnedDHTRecordPointer extends $pb.GeneratedMessage { KeyPair ensureOwner() => $_ensure(1); } +class Chat extends $pb.GeneratedMessage { + factory Chat() => create(); + Chat._() : super(); + factory Chat.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory Chat.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'Chat', createEmptyInstance: create) + ..e(1, _omitFieldNames ? '' : 'type', $pb.PbFieldType.OE, defaultOrMaker: ChatType.CHAT_TYPE_UNSPECIFIED, valueOf: ChatType.valueOf, enumValues: ChatType.values) + ..aOM(2, _omitFieldNames ? '' : 'remoteConversationKey', subBuilder: TypedKey.create) + ..hasRequiredFields = false + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + Chat clone() => Chat()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + Chat copyWith(void Function(Chat) updates) => super.copyWith((message) => updates(message as Chat)) as Chat; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static Chat create() => Chat._(); + Chat createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static Chat getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static Chat? _defaultInstance; + + @$pb.TagNumber(1) + ChatType get type => $_getN(0); + @$pb.TagNumber(1) + set type(ChatType v) { setField(1, v); } + @$pb.TagNumber(1) + $core.bool hasType() => $_has(0); + @$pb.TagNumber(1) + void clearType() => clearField(1); + + @$pb.TagNumber(2) + TypedKey get remoteConversationKey => $_getN(1); + @$pb.TagNumber(2) + set remoteConversationKey(TypedKey v) { setField(2, v); } + @$pb.TagNumber(2) + $core.bool hasRemoteConversationKey() => $_has(1); + @$pb.TagNumber(2) + void clearRemoteConversationKey() => clearField(2); + @$pb.TagNumber(2) + TypedKey ensureRemoteConversationKey() => $_ensure(1); +} + class Account extends $pb.GeneratedMessage { factory Account() => create(); Account._() : super(); @@ -1241,6 +1307,7 @@ class Account extends $pb.GeneratedMessage { ..a<$core.int>(3, _omitFieldNames ? '' : 'autoAwayTimeoutSec', $pb.PbFieldType.OU3) ..aOM(4, _omitFieldNames ? '' : 'contactList', subBuilder: OwnedDHTRecordPointer.create) ..aOM(5, _omitFieldNames ? '' : 'contactInvitationRecords', subBuilder: OwnedDHTRecordPointer.create) + ..aOM(6, _omitFieldNames ? '' : 'chatList', subBuilder: OwnedDHTRecordPointer.create) ..hasRequiredFields = false ; @@ -1315,6 +1382,17 @@ class Account extends $pb.GeneratedMessage { void clearContactInvitationRecords() => clearField(5); @$pb.TagNumber(5) OwnedDHTRecordPointer ensureContactInvitationRecords() => $_ensure(4); + + @$pb.TagNumber(6) + OwnedDHTRecordPointer get chatList => $_getN(5); + @$pb.TagNumber(6) + set chatList(OwnedDHTRecordPointer v) { setField(6, v); } + @$pb.TagNumber(6) + $core.bool hasChatList() => $_has(5); + @$pb.TagNumber(6) + void clearChatList() => clearField(6); + @$pb.TagNumber(6) + OwnedDHTRecordPointer ensureChatList() => $_ensure(5); } class ContactInvitation extends $pb.GeneratedMessage { diff --git a/lib/entities/proto/veilidchat.pbenum.dart b/lib/entities/proto/veilidchat.pbenum.dart index 55a0323..7bef00f 100644 --- a/lib/entities/proto/veilidchat.pbenum.dart +++ b/lib/entities/proto/veilidchat.pbenum.dart @@ -51,6 +51,23 @@ class Availability extends $pb.ProtobufEnum { const Availability._($core.int v, $core.String n) : super(v, n); } +class ChatType extends $pb.ProtobufEnum { + static const ChatType CHAT_TYPE_UNSPECIFIED = ChatType._(0, _omitEnumNames ? '' : 'CHAT_TYPE_UNSPECIFIED'); + static const ChatType SINGLE_CONTACT = ChatType._(1, _omitEnumNames ? '' : 'SINGLE_CONTACT'); + static const ChatType GROUP = ChatType._(2, _omitEnumNames ? '' : 'GROUP'); + + static const $core.List values = [ + CHAT_TYPE_UNSPECIFIED, + SINGLE_CONTACT, + GROUP, + ]; + + static final $core.Map<$core.int, ChatType> _byValue = $pb.ProtobufEnum.initByValue(values); + static ChatType? valueOf($core.int value) => _byValue[value]; + + const ChatType._($core.int v, $core.String n) : super(v, n); +} + 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'); diff --git a/lib/entities/proto/veilidchat.pbjson.dart b/lib/entities/proto/veilidchat.pbjson.dart index ee6d5c4..eefc75f 100644 --- a/lib/entities/proto/veilidchat.pbjson.dart +++ b/lib/entities/proto/veilidchat.pbjson.dart @@ -46,6 +46,21 @@ final $typed_data.Uint8List availabilityDescriptor = $convert.base64Decode( 'lMSVRZX09GRkxJTkUQARIVChFBVkFJTEFCSUxJVFlfRlJFRRACEhUKEUFWQUlMQUJJTElUWV9C' 'VVNZEAMSFQoRQVZBSUxBQklMSVRZX0FXQVkQBA=='); +@$core.Deprecated('Use chatTypeDescriptor instead') +const ChatType$json = { + '1': 'ChatType', + '2': [ + {'1': 'CHAT_TYPE_UNSPECIFIED', '2': 0}, + {'1': 'SINGLE_CONTACT', '2': 1}, + {'1': 'GROUP', '2': 2}, + ], +}; + +/// Descriptor for `ChatType`. Decode as a `google.protobuf.EnumDescriptorProto`. +final $typed_data.Uint8List chatTypeDescriptor = $convert.base64Decode( + 'CghDaGF0VHlwZRIZChVDSEFUX1RZUEVfVU5TUEVDSUZJRUQQABISCg5TSU5HTEVfQ09OVEFDVB' + 'ABEgkKBUdST1VQEAI='); + @$core.Deprecated('Use encryptionKeyTypeDescriptor instead') const EncryptionKeyType$json = { '1': 'EncryptionKeyType', @@ -269,16 +284,16 @@ const Conversation$json = { '1': 'Conversation', '2': [ {'1': 'profile', '3': 1, '4': 1, '5': 11, '6': '.Profile', '10': 'profile'}, - {'1': 'identity', '3': 2, '4': 1, '5': 9, '10': 'identity'}, - {'1': 'messages', '3': 3, '4': 1, '5': 11, '6': '.DHTLog', '10': 'messages'}, + {'1': 'identity_master_json', '3': 2, '4': 1, '5': 9, '10': 'identityMasterJson'}, + {'1': 'messages', '3': 3, '4': 1, '5': 11, '6': '.OwnedDHTRecordPointer', '10': 'messages'}, ], }; /// Descriptor for `Conversation`. Decode as a `google.protobuf.DescriptorProto`. final $typed_data.Uint8List conversationDescriptor = $convert.base64Decode( - 'CgxDb252ZXJzYXRpb24SIgoHcHJvZmlsZRgBIAEoCzIILlByb2ZpbGVSB3Byb2ZpbGUSGgoIaW' - 'RlbnRpdHkYAiABKAlSCGlkZW50aXR5EiMKCG1lc3NhZ2VzGAMgASgLMgcuREhUTG9nUghtZXNz' - 'YWdlcw=='); + 'CgxDb252ZXJzYXRpb24SIgoHcHJvZmlsZRgBIAEoCzIILlByb2ZpbGVSB3Byb2ZpbGUSMAoUaW' + 'RlbnRpdHlfbWFzdGVyX2pzb24YAiABKAlSEmlkZW50aXR5TWFzdGVySnNvbhIyCghtZXNzYWdl' + 'cxgDIAEoCzIWLk93bmVkREhUUmVjb3JkUG9pbnRlclIIbWVzc2FnZXM='); @$core.Deprecated('Use contactDescriptor instead') const Contact$json = { @@ -286,21 +301,24 @@ const Contact$json = { '2': [ {'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_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'}, + {'1': 'identity_master_json', '3': 3, '4': 1, '5': 9, '10': 'identityMasterJson'}, + {'1': 'identity_public_key', '3': 4, '4': 1, '5': 11, '6': '.TypedKey', '10': 'identityPublicKey'}, + {'1': 'remote_conversation_key', '3': 5, '4': 1, '5': 11, '6': '.TypedKey', '10': 'remoteConversationKey'}, + {'1': 'local_conversation', '3': 6, '4': 1, '5': 11, '6': '.OwnedDHTRecordPointer', '10': 'localConversation'}, + {'1': 'show_availability', '3': 7, '4': 1, '5': 8, '10': 'showAvailability'}, ], }; /// Descriptor for `Contact`. Decode as a `google.protobuf.DescriptorProto`. final $typed_data.Uint8List contactDescriptor = $convert.base64Decode( 'CgdDb250YWN0Ei8KDmVkaXRlZF9wcm9maWxlGAEgASgLMgguUHJvZmlsZVINZWRpdGVkUHJvZm' - 'lsZRIvCg5yZW1vdGVfcHJvZmlsZRgCIAEoCzIILlByb2ZpbGVSDXJlbW90ZVByb2ZpbGUSJwoP' - 'cmVtb3RlX2lkZW50aXR5GAMgASgJUg5yZW1vdGVJZGVudGl0eRJBChdyZW1vdGVfY29udmVyc2' - 'F0aW9uX2tleRgEIAEoCzIJLlR5cGVkS2V5UhVyZW1vdGVDb252ZXJzYXRpb25LZXkSRQoSbG9j' - 'YWxfY29udmVyc2F0aW9uGAUgASgLMhYuT3duZWRESFRSZWNvcmRQb2ludGVyUhFsb2NhbENvbn' - 'ZlcnNhdGlvbhIrChFzaG93X2F2YWlsYWJpbGl0eRgGIAEoCFIQc2hvd0F2YWlsYWJpbGl0eQ=='); + 'lsZRIvCg5yZW1vdGVfcHJvZmlsZRgCIAEoCzIILlByb2ZpbGVSDXJlbW90ZVByb2ZpbGUSMAoU' + 'aWRlbnRpdHlfbWFzdGVyX2pzb24YAyABKAlSEmlkZW50aXR5TWFzdGVySnNvbhI5ChNpZGVudG' + 'l0eV9wdWJsaWNfa2V5GAQgASgLMgkuVHlwZWRLZXlSEWlkZW50aXR5UHVibGljS2V5EkEKF3Jl' + 'bW90ZV9jb252ZXJzYXRpb25fa2V5GAUgASgLMgkuVHlwZWRLZXlSFXJlbW90ZUNvbnZlcnNhdG' + 'lvbktleRJFChJsb2NhbF9jb252ZXJzYXRpb24YBiABKAsyFi5Pd25lZERIVFJlY29yZFBvaW50' + 'ZXJSEWxvY2FsQ29udmVyc2F0aW9uEisKEXNob3dfYXZhaWxhYmlsaXR5GAcgASgIUhBzaG93QX' + 'ZhaWxhYmlsaXR5'); @$core.Deprecated('Use profileDescriptor instead') const Profile$json = { @@ -338,6 +356,20 @@ final $typed_data.Uint8List ownedDHTRecordPointerDescriptor = $convert.base64Dec 'ChVPd25lZERIVFJlY29yZFBvaW50ZXISKAoKcmVjb3JkX2tleRgBIAEoCzIJLlR5cGVkS2V5Ug' 'lyZWNvcmRLZXkSHgoFb3duZXIYAiABKAsyCC5LZXlQYWlyUgVvd25lcg=='); +@$core.Deprecated('Use chatDescriptor instead') +const Chat$json = { + '1': 'Chat', + '2': [ + {'1': 'type', '3': 1, '4': 1, '5': 14, '6': '.ChatType', '10': 'type'}, + {'1': 'remote_conversation_key', '3': 2, '4': 1, '5': 11, '6': '.TypedKey', '10': 'remoteConversationKey'}, + ], +}; + +/// Descriptor for `Chat`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List chatDescriptor = $convert.base64Decode( + 'CgRDaGF0Eh0KBHR5cGUYASABKA4yCS5DaGF0VHlwZVIEdHlwZRJBChdyZW1vdGVfY29udmVyc2' + 'F0aW9uX2tleRgCIAEoCzIJLlR5cGVkS2V5UhVyZW1vdGVDb252ZXJzYXRpb25LZXk='); + @$core.Deprecated('Use accountDescriptor instead') const Account$json = { '1': 'Account', @@ -347,6 +379,7 @@ const Account$json = { {'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_invitation_records', '3': 5, '4': 1, '5': 11, '6': '.OwnedDHTRecordPointer', '10': 'contactInvitationRecords'}, + {'1': 'chat_list', '3': 6, '4': 1, '5': 11, '6': '.OwnedDHTRecordPointer', '10': 'chatList'}, ], }; @@ -356,7 +389,8 @@ final $typed_data.Uint8List accountDescriptor = $convert.base64Decode( 'JsZRgCIAEoCFIJaW52aXNpYmxlEjEKFWF1dG9fYXdheV90aW1lb3V0X3NlYxgDIAEoDVISYXV0' 'b0F3YXlUaW1lb3V0U2VjEjkKDGNvbnRhY3RfbGlzdBgEIAEoCzIWLk93bmVkREhUUmVjb3JkUG' '9pbnRlclILY29udGFjdExpc3QSVAoaY29udGFjdF9pbnZpdGF0aW9uX3JlY29yZHMYBSABKAsy' - 'Fi5Pd25lZERIVFJlY29yZFBvaW50ZXJSGGNvbnRhY3RJbnZpdGF0aW9uUmVjb3Jkcw=='); + 'Fi5Pd25lZERIVFJlY29yZFBvaW50ZXJSGGNvbnRhY3RJbnZpdGF0aW9uUmVjb3JkcxIzCgljaG' + 'F0X2xpc3QYBiABKAsyFi5Pd25lZERIVFJlY29yZFBvaW50ZXJSCGNoYXRMaXN0'); @$core.Deprecated('Use contactInvitationDescriptor instead') const ContactInvitation$json = { diff --git a/lib/entities/veilidchat.proto b/lib/entities/veilidchat.proto index 3f03158..027bdae 100644 --- a/lib/entities/veilidchat.proto +++ b/lib/entities/veilidchat.proto @@ -181,9 +181,9 @@ message Conversation { // Profile to publish to friend Profile profile = 1; // Identity master (JSON) to publish to friend - string identity = 2; + string identity_master_json = 2; // Messages DHTLog - DHTLog messages = 3; + OwnedDHTRecordPointer messages = 3; } // A record of a contact that has accepted a contact invitation @@ -198,14 +198,16 @@ message Contact { Profile edited_profile = 1; // Copy of friend's profile from remote conversation Profile remote_profile = 2; - // Copy of friend's identity (JSON) from remote conversation - string remote_identity = 3; + // Copy of friend's IdentityMaster in JSON from remote conversation + string identity_master_json = 3; + // Copy of friend's most recent identity public key from their identityMaster + TypedKey identity_public_key = 4; // Remote conversation key to sync from friend - TypedKey remote_conversation_key = 4; + TypedKey remote_conversation_key = 5; // Our conversation key for friend to sync - OwnedDHTRecordPointer local_conversation = 5; + OwnedDHTRecordPointer local_conversation = 6; // Show availability - bool show_availability = 6; + bool show_availability = 7; } // Contact availability @@ -243,6 +245,20 @@ message OwnedDHTRecordPointer { KeyPair owner = 2; } +enum ChatType { + CHAT_TYPE_UNSPECIFIED = 0; + SINGLE_CONTACT = 1; + GROUP = 2; +} + +// Either a 1-1 converation or a group chat (eventually) +message Chat { + // What kind of chat is this + ChatType type = 1; + // 1-1 Chat key + TypedKey remote_conversation_key = 2; +} + // A record of an individual account // Pointed to by the identity account map in the identity key // @@ -261,6 +277,10 @@ message Account { // The ContactInvitationRecord DHTShortArray for this account // DHT Private OwnedDHTRecordPointer contact_invitation_records = 5; + // The chats DHTList for this account + // DHT Private + OwnedDHTRecordPointer chat_list = 6; + } // EncryptionKeyType diff --git a/lib/pages/home.dart b/lib/pages/home.dart index ccd38b6..6c087a0 100644 --- a/lib/pages/home.dart +++ b/lib/pages/home.dart @@ -9,11 +9,9 @@ import 'package:signal_strength_indicator/signal_strength_indicator.dart'; import '../components/chat_component.dart'; import '../providers/account.dart'; import '../providers/contact.dart'; -import '../providers/local_accounts.dart'; -import '../providers/logins.dart'; +import '../providers/contact_invite.dart'; import '../providers/window_control.dart'; import '../tools/tools.dart'; -import '../veilid_support/dht_support/dht_record_pool.dart'; import 'main_pager/main_pager.dart'; class HomePage extends ConsumerStatefulWidget { @@ -91,8 +89,16 @@ class HomePageState extends ConsumerState activeAccountInfo: activeAccountInfo, contactInvitationRecord: contactInvitationRecord); if (acceptReject != null) { - if (acceptReject) { + final acceptedContact = acceptReject.acceptedContact; + if (acceptedContact != null) { // Accept + await createContact( + activeAccountInfo: activeAccountInfo, + profile: acceptedContact.profile, + remoteIdentity: acceptedContact.remoteIdentity, + remoteConversationKey: acceptedContact.remoteConversationKey, + localConversation: acceptedContact.localConversation, + ); ref ..invalidate(fetchContactInvitationRecordsProvider) ..invalidate(fetchContactListProvider); diff --git a/lib/pages/main_pager/account_page.dart b/lib/pages/main_pager/account_page.dart index 1b74166..45427c4 100644 --- a/lib/pages/main_pager/account_page.dart +++ b/lib/pages/main_pager/account_page.dart @@ -12,6 +12,7 @@ import '../../entities/local_account.dart'; import '../../entities/proto.dart' as proto; import '../../providers/account.dart'; import '../../providers/contact.dart'; +import '../../providers/contact_invite.dart'; import '../../providers/local_accounts.dart'; import '../../providers/logins.dart'; import '../../tools/tools.dart'; diff --git a/lib/providers/contact.dart b/lib/providers/contact.dart index b1065f4..fed41dc 100644 --- a/lib/providers/contact.dart +++ b/lib/providers/contact.dart @@ -1,447 +1,22 @@ import 'dart:convert'; -import 'dart:typed_data'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; -import 'package:fixnum/fixnum.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; import '../entities/identity.dart'; -import '../entities/local_account.dart'; import '../entities/proto.dart' as proto; -import '../entities/proto.dart' - show - Contact, - ContactInvitation, - ContactInvitationRecord, - ContactRequest, - ContactRequestPrivate, - ContactResponse, - SignedContactInvitation, - SignedContactResponse; -import '../log/loggy.dart'; -import '../tools/tools.dart'; +import '../entities/proto.dart' show Contact; + import '../veilid_support/veilid_support.dart'; import 'account.dart'; part 'contact.g.dart'; -Future checkAcceptRejectContact( - {required ActiveAccountInfo activeAccountInfo, - required ContactInvitationRecord contactInvitationRecord}) async { - // Open the contact request inbox - try { - final pool = await DHTRecordPool.instance(); - final accountRecordKey = - activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey; - final writerKey = - proto.CryptoKeyProto.fromProto(contactInvitationRecord.writerKey); - final writerSecret = - proto.CryptoKeyProto.fromProto(contactInvitationRecord.writerSecret); - final writer = TypedKeyPair( - kind: contactInvitationRecord.contactRequestInbox.recordKey.kind, - key: writerKey, - secret: writerSecret); - final acceptReject = await (await pool.openRead( - proto.TypedKeyProto.fromProto( - contactInvitationRecord.contactRequestInbox.recordKey), - crypto: await DHTRecordCryptoPrivate.fromTypedKeyPair(writer), - parent: accountRecordKey, - defaultSubkey: 1)) - .scope((contactRequestInbox) async { - // - final signedContactResponse = await contactRequestInbox - .getProtobuf(SignedContactResponse.fromBuffer, forceRefresh: true); - if (signedContactResponse == null) { - return null; - } - - final contactResponseBytes = - Uint8List.fromList(signedContactResponse.contactResponse); - final contactResponse = ContactResponse.fromBuffer(contactResponseBytes); - final contactIdentityMasterRecordKey = proto.TypedKeyProto.fromProto( - contactResponse.identityMasterRecordKey); - final cs = await pool.veilid.getCryptoSystem( - contactInvitationRecord.contactRequestInbox.recordKey.kind); - - // Fetch the remote contact's account master - final contactIdentityMaster = await openIdentityMaster( - identityMasterRecordKey: contactIdentityMasterRecordKey); - - // Verify - final signature = proto.SignatureProto.fromProto( - signedContactResponse.identitySignature); - try { - await cs.verify(contactIdentityMaster.identityPublicKey, - contactResponseBytes, signature); - } on Exception catch (e) { - log.error('Bad identity used, failed to verify: $e'); - return false; - } - return contactResponse.accept; - }); - - if (acceptReject == null) { - return null; - } - - // Add contact if accepted - if (acceptReject) { - // - await deleteContactInvitation( - accepted: true, - activeAccountInfo: activeAccountInfo, - contactInvitationRecord: contactInvitationRecord); - return true; - } else { - await deleteContactInvitation( - accepted: false, - activeAccountInfo: activeAccountInfo, - contactInvitationRecord: contactInvitationRecord); - return false; - } - } on Exception catch (e) { - log.error('Exception in checkAcceptRejectContact: $e'); - return null; - } -} - -Future deleteContactInvitation( - {required bool accepted, - 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)) - .scope((contactRequestInbox) async { - // Wipe out old invitation so it shows up as invalid - await contactRequestInbox.tryWriteBytes(Uint8List(0)); - await contactRequestInbox.delete(); - }); - if (!accepted) { - await (await pool.openOwned( - proto.OwnedDHTRecordPointerProto.fromProto( - contactInvitationRecord.localConversation), - parent: accountRecordKey)) - .delete(); - } - }); -} - -Future createContactInvitation( - {required ActiveAccountInfo activeAccountInfo, - required EncryptionKeyType encryptionKeyType, - required String encryptionKey, - required String message, - required 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((localConversation) async { - // Make ContactRequestPrivate and encrypt with the writer secret - final crpriv = ContactRequestPrivate() - ..writerKey = writer.key.toProto() - ..profile = activeAccountInfo.account.profile - ..identityMasterRecordKey = - activeAccountInfo.userLogin.accountMasterRecordKey.toProto() - ..chatRecordKey = localConversation.key.toProto() - ..expiration = expiration?.toInt64() ?? Int64.ZERO; - final crprivbytes = crpriv.writeToBuffer(); - final encryptedContactRequestPrivate = - await cs.encryptNoAuthWithNonce(crprivbytes, writer.secret); - - // Create ContactRequest and embed contactrequestprivate - final creq = 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((contactRequestInbox) async { - // Store ContactRequest in owner subkey - await contactRequestInbox.eventualWriteProtobuf(creq); - - // Create ContactInvitation and SignedContactInvitation - final cinv = ContactInvitation() - ..contactRequestInboxKey = contactRequestInbox.key.toProto() - ..writerSecret = encryptedSecret; - final cinvbytes = cinv.writeToBuffer(); - final scinv = SignedContactInvitation() - ..contactInvitation = cinvbytes - ..identitySignature = - (await cs.sign(identityKey, identitySecret, cinvbytes)).toProto(); - signedContactInvitationBytes = scinv.writeToBuffer(); - - // Create ContactInvitationRecord - final cinvrec = ContactInvitationRecord() - ..contactRequestInbox = - contactRequestInbox.ownedDHTRecordPointer.toProto() - ..writerKey = writer.key.toProto() - ..writerSecret = writer.secret.toProto() - ..localConversation = localConversation.ownedDHTRecordPointer.toProto() - ..expiration = expiration?.toInt64() ?? Int64.ZERO - ..invitation = signedContactInvitationBytes - ..message = message; - - // 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( - 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; -} - -class ValidContactInvitation { - ValidContactInvitation( - {required this.signedContactInvitation, - required this.contactInvitation, - required this.contactRequestInboxKey, - required this.contactRequest, - required this.contactRequestPrivate, - required this.contactIdentityMaster, - required this.writer}); - - SignedContactInvitation signedContactInvitation; - ContactInvitation contactInvitation; - TypedKey contactRequestInboxKey; - ContactRequest contactRequest; - ContactRequestPrivate contactRequestPrivate; - IdentityMaster contactIdentityMaster; - KeyPair writer; -} - -typedef GetEncryptionKeyCallback = Future Function( - EncryptionKeyType encryptionKeyType, Uint8List encryptedSecret); - -Future validateContactInvitation(Uint8List inviteData, - GetEncryptionKeyCallback getEncryptionKeyCallback) async { - 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); - - late final ValidContactInvitation out; - - 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); - final writerSecret = await getEncryptionKeyCallback( - encryptionKeyType, Uint8List.fromList(contactInvitation.writerSecret)); - - 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); - - final writer = KeyPair( - key: proto.CryptoKeyProto.fromProto(contactRequestPrivate.writerKey), - secret: writerSecret); - - out = ValidContactInvitation( - signedContactInvitation: signedContactInvitation, - contactInvitation: contactInvitation, - contactRequestInboxKey: contactRequestInboxKey, - contactRequest: contactRequest, - contactRequestPrivate: contactRequestPrivate, - contactIdentityMaster: contactIdentityMaster, - writer: writer); - }); - - return out; -} - -class AcceptedContact { - AcceptedContact({ - required this.profile, - required this.remoteIdentity, - required this.remoteConversation, - required this.localConversation, - }); - - proto.Profile profile; - IdentityMaster remoteIdentity; - TypedKey remoteConversation; - OwnedDHTRecordPointer localConversation; -} - -Future acceptContactInvitation( - ActiveAccountInfo activeAccountInfo, - ValidContactInvitation validContactInvitation) async { - final pool = await DHTRecordPool.instance(); - final accountRecordKey = - activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey; - - return (await pool.openWrite(validContactInvitation.contactRequestInboxKey, - validContactInvitation.writer)) - .deleteScope((contactRequestInbox) async { - final cs = await pool.veilid - .getCryptoSystem(validContactInvitation.contactRequestInboxKey.kind); - - // Create local conversation key for this - // contact and send via contact response - return (await pool.create(parent: accountRecordKey)) - .deleteScope((localConversation) async { - final contactResponse = ContactResponse() - ..accept = true - ..remoteConversationKey = localConversation.key.toProto() - ..identityMasterRecordKey = activeAccountInfo - .localAccount.identityMaster.masterRecordKey - .toProto(); - final contactResponseBytes = contactResponse.writeToBuffer(); - - final identitySignature = await cs.sign( - activeAccountInfo.localAccount.identityMaster.identityPublicKey, - activeAccountInfo.userLogin.identitySecret.value, - contactResponseBytes); - - final signedContactResponse = SignedContactResponse() - ..contactResponse = contactResponseBytes - ..identitySignature = identitySignature.toProto(); - - // Write the acceptance to the inbox - if (await contactRequestInbox.tryWriteProtobuf( - SignedContactResponse.fromBuffer, signedContactResponse, - subkey: 1) != - null) { - log.error('failed to accept contact invitation'); - await localConversation.delete(); - await contactRequestInbox.delete(); - return null; - } - return AcceptedContact( - profile: validContactInvitation.contactRequestPrivate.profile, - remoteIdentity: validContactInvitation.contactIdentityMaster, - remoteConversation: proto.TypedKeyProto.fromProto( - validContactInvitation.contactRequestPrivate.chatRecordKey), - localConversation: localConversation.ownedDHTRecordPointer, - ); - }); - }); -} - -Future rejectContactInvitation(ActiveAccountInfo activeAccountInfo, - ValidContactInvitation validContactInvitation) async { - final pool = await DHTRecordPool.instance(); - return (await pool.openWrite(validContactInvitation.contactRequestInboxKey, - validContactInvitation.writer)) - .deleteScope((contactRequestInbox) async { - final cs = await pool.veilid - .getCryptoSystem(validContactInvitation.contactRequestInboxKey.kind); - - final contactResponse = ContactResponse() - ..accept = false - ..identityMasterRecordKey = activeAccountInfo - .localAccount.identityMaster.masterRecordKey - .toProto(); - final contactResponseBytes = contactResponse.writeToBuffer(); - - final identitySignature = await cs.sign( - activeAccountInfo.localAccount.identityMaster.identityPublicKey, - activeAccountInfo.userLogin.identitySecret.value, - contactResponseBytes); - - final signedContactResponse = SignedContactResponse() - ..contactResponse = contactResponseBytes - ..identitySignature = identitySignature.toProto(); - - // Write the rejection to the invox - if (await contactRequestInbox.tryWriteProtobuf( - SignedContactResponse.fromBuffer, signedContactResponse, - subkey: 1) != - null) { - log.error('failed to reject contact invitation'); - return false; - } - return true; - }); -} - Future createContact({ required ActiveAccountInfo activeAccountInfo, required proto.Profile profile, required IdentityMaster remoteIdentity, - required TypedKey remoteConversation, + required TypedKey remoteConversationKey, required OwnedDHTRecordPointer localConversation, }) async { final accountRecordKey = @@ -451,8 +26,12 @@ Future createContact({ final contact = Contact() ..editedProfile = profile ..remoteProfile = profile - ..remoteIdentity = jsonEncode(remoteIdentity.toJson()) - ..remoteConversationKey = remoteConversation.toProto() + ..identityMasterJson = jsonEncode(remoteIdentity.toJson()) + ..identityPublicKey = TypedKey( + kind: remoteIdentity.identityRecordKey.kind, + value: remoteIdentity.identityPublicKey) + .toProto() + ..remoteConversationKey = remoteConversationKey.toProto() ..localConversation = localConversation.toProto() ..showAvailability = false; @@ -469,35 +48,40 @@ Future createContact({ }); } -/// Get the active account contact invitation list -@riverpod -Future?> fetchContactInvitationRecords( - FetchContactInvitationRecordsRef ref) async { - // See if we've logged into this account or if it is locked - final activeAccountInfo = await ref.watch(fetchActiveAccountProvider.future); - if (activeAccountInfo == null) { - return null; - } +Future deleteContact( + {required ActiveAccountInfo activeAccountInfo, + required Contact contact}) async { + final pool = await DHTRecordPool.instance(); final accountRecordKey = activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey; - // Decode the contact invitation list from the DHT - IList out = const IListConst([]); + // Remove Contact from account's list await (await DHTShortArray.openOwned( proto.OwnedDHTRecordPointerProto.fromProto( - activeAccountInfo.account.contactInvitationRecords), + activeAccountInfo.account.contactList), parent: accountRecordKey)) - .scope((cirList) async { - for (var i = 0; i < cirList.length; i++) { - final cir = await cirList.getItem(i); - if (cir == null) { - throw StateError('Failed to get contact invitation record'); + .scope((contactList) async { + for (var i = 0; i < contactList.length; i++) { + final item = + await contactList.getItemProtobuf(proto.Contact.fromBuffer, i); + if (item == null) { + throw StateError('Failed to get contact'); + } + if (item.remoteConversationKey == contact.remoteConversationKey) { + await contactList.tryRemoveItem(i); + break; } - out = out.add(ContactInvitationRecord.fromBuffer(cir)); } + await (await pool.openOwned( + proto.OwnedDHTRecordPointerProto.fromProto( + contact.localConversation), + parent: accountRecordKey)) + .delete(); + await (await pool.openRead( + proto.TypedKeyProto.fromProto(contact.remoteConversationKey), + parent: accountRecordKey)) + .delete(); }); - - return out; } /// Get the active account contact list diff --git a/lib/providers/contact.g.dart b/lib/providers/contact.g.dart index f6b7f45..aa98b33 100644 --- a/lib/providers/contact.g.dart +++ b/lib/providers/contact.g.dart @@ -6,26 +6,6 @@ part of 'contact.dart'; // RiverpodGenerator // ************************************************************************** -String _$fetchContactInvitationRecordsHash() => - r'fcedc1807c6cb25ac6c2c42b372ec04abd4b911f'; - -/// Get the active account contact invitation list -/// -/// Copied from [fetchContactInvitationRecords]. -@ProviderFor(fetchContactInvitationRecords) -final fetchContactInvitationRecordsProvider = - AutoDisposeFutureProvider?>.internal( - fetchContactInvitationRecords, - name: r'fetchContactInvitationRecordsProvider', - debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') - ? null - : _$fetchContactInvitationRecordsHash, - dependencies: null, - allTransitiveDependencies: null, -); - -typedef FetchContactInvitationRecordsRef - = AutoDisposeFutureProviderRef?>; String _$fetchContactListHash() => r'60ae4f117fc51c0870449563aedca7baf51cc254'; /// Get the active account contact list