From d3ecae011323b5dc89a867fea3df87657f53774e Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Mon, 18 Dec 2023 20:05:23 -0500 Subject: [PATCH 01/68] updates and refactor protobuf --- ios/Podfile.lock | 8 +- lib/entities/local_account.freezed.dart | 2 +- lib/entities/preferences.freezed.dart | 6 +- lib/entities/user_login.freezed.dart | 4 +- lib/providers/chat.dart | 15 +- lib/providers/connection_state.freezed.dart | 2 +- lib/providers/contact.dart | 11 +- lib/providers/contact.g.dart | 7 +- lib/providers/contact_invite.dart | 60 ++-- lib/providers/conversation.dart | 38 +-- .../src/dht_record_pool.freezed.dart | 4 +- lib/veilid_support/src/config.dart | 27 +- lib/veilid_support/src/identity.freezed.dart | 6 +- macos/Podfile.lock | 4 +- pubspec.lock | 318 +++++++++--------- pubspec.yaml | 2 +- 16 files changed, 266 insertions(+), 248 deletions(-) diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 5ba25f2..c858771 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -55,7 +55,7 @@ PODS: - GTMSessionFetcher/Core (< 3.0, >= 1.1) - MLImage (= 1.0.0-beta4) - MLKitCommon (~> 9.0) - - mobile_scanner (3.2.0): + - mobile_scanner (3.5.5): - Flutter - GoogleMLKit/BarcodeScanning (~> 4.0.0) - nanopb (2.30909.0): @@ -160,17 +160,17 @@ SPEC CHECKSUMS: MLKitBarcodeScanning: 04e264482c5f3810cb89ebc134ef6b61e67db505 MLKitCommon: c1b791c3e667091918d91bda4bba69a91011e390 MLKitVision: 8baa5f46ee3352614169b85250574fde38c36f49 - mobile_scanner: 47056db0c04027ea5f41a716385542da28574662 + mobile_scanner: 202ab6f652e40a9add68b10de4c4fb2a745c4348 nanopb: b552cce312b6c8484180ef47159bc0f65a1f0431 pasteboard: 982969ebaa7c78af3e6cc7761e8f5e77565d9ce0 path_provider_foundation: 29f094ae23ebbca9d3d0cec13889cd9060c0e943 PromisesObjC: c50d2056b5253dadbd6c2bea79b0674bd5a52fa4 - share_plus: 599aa54e4ea31d4b4c0e9c911bcc26c55e791028 + share_plus: c3fef564749587fc939ef86ffb283ceac0baf9f5 shared_preferences_foundation: 5b919d13b803cadd15ed2dc053125c68730e5126 smart_auth: 4bedbc118723912d0e45a07e8ab34039c19e04f2 sqflite: 31f7eba61e3074736dff8807a9b41581e4f7f15a system_info_plus: 5393c8da281d899950d751713575fbf91c7709aa - url_launcher_ios: 08a3dfac5fb39e8759aeb0abbd5d9480f30fc8b4 + url_launcher_ios: bf5ce03e0e2088bad9cc378ea97fa0ed5b49673b veilid: f5c2e662f91907b30cf95762619526ac3e4512fd PODFILE CHECKSUM: 7f4cf2154d55730d953b184299e6feee7a274740 diff --git a/lib/entities/local_account.freezed.dart b/lib/entities/local_account.freezed.dart index 19dcd76..a720f3f 100644 --- a/lib/entities/local_account.freezed.dart +++ b/lib/entities/local_account.freezed.dart @@ -225,7 +225,7 @@ class _$LocalAccountImpl implements _LocalAccount { } @override - bool operator ==(dynamic other) { + bool operator ==(Object other) { return identical(this, other) || (other.runtimeType == runtimeType && other is _$LocalAccountImpl && diff --git a/lib/entities/preferences.freezed.dart b/lib/entities/preferences.freezed.dart index 7020dcb..0e951e0 100644 --- a/lib/entities/preferences.freezed.dart +++ b/lib/entities/preferences.freezed.dart @@ -146,7 +146,7 @@ class _$LockPreferenceImpl implements _LockPreference { } @override - bool operator ==(dynamic other) { + bool operator ==(Object other) { return identical(this, other) || (other.runtimeType == runtimeType && other is _$LockPreferenceImpl && @@ -332,7 +332,7 @@ class _$ThemePreferencesImpl implements _ThemePreferences { } @override - bool operator ==(dynamic other) { + bool operator ==(Object other) { return identical(this, other) || (other.runtimeType == runtimeType && other is _$ThemePreferencesImpl && @@ -541,7 +541,7 @@ class _$PreferencesImpl implements _Preferences { } @override - bool operator ==(dynamic other) { + bool operator ==(Object other) { return identical(this, other) || (other.runtimeType == runtimeType && other is _$PreferencesImpl && diff --git a/lib/entities/user_login.freezed.dart b/lib/entities/user_login.freezed.dart index aca29fc..9aa3b68 100644 --- a/lib/entities/user_login.freezed.dart +++ b/lib/entities/user_login.freezed.dart @@ -182,7 +182,7 @@ class _$UserLoginImpl implements _UserLogin { } @override - bool operator ==(dynamic other) { + bool operator ==(Object other) { return identical(this, other) || (other.runtimeType == runtimeType && other is _$UserLoginImpl && @@ -358,7 +358,7 @@ class _$ActiveLoginsImpl implements _ActiveLogins { } @override - bool operator ==(dynamic other) { + bool operator ==(Object other) { return identical(this, other) || (other.runtimeType == runtimeType && other is _$ActiveLoginsImpl && diff --git a/lib/providers/chat.dart b/lib/providers/chat.dart index 7ddbe1d..d64c4b9 100644 --- a/lib/providers/chat.dart +++ b/lib/providers/chat.dart @@ -3,7 +3,6 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; import '../proto/proto.dart' as proto; -import '../proto/proto.dart' show Chat, ChatType; import '../veilid_support/veilid_support.dart'; import 'account.dart'; @@ -19,8 +18,8 @@ Future getOrCreateChatSingleContact({ activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey; // Create conversation type Chat - final chat = Chat() - ..type = ChatType.SINGLE_CONTACT + final chat = proto.Chat() + ..type = proto.ChatType.SINGLE_CONTACT ..remoteConversationKey = remoteConversationRecordKey.toProto(); // Add Chat to account's list @@ -35,7 +34,7 @@ Future getOrCreateChatSingleContact({ if (cbuf == null) { throw Exception('Failed to get chat'); } - final c = Chat.fromBuffer(cbuf); + final c = proto.Chat.fromBuffer(cbuf); if (c == chat) { return; } @@ -68,7 +67,7 @@ Future deleteChat( if (cbuf == null) { throw Exception('Failed to get chat'); } - final c = Chat.fromBuffer(cbuf); + final c = proto.Chat.fromBuffer(cbuf); if (c.remoteConversationKey == remoteConversationKey) { await chatList.tryRemoveItem(i); @@ -84,7 +83,7 @@ Future deleteChat( /// Get the active account contact list @riverpod -Future?> fetchChatList(FetchChatListRef ref) async { +Future?> fetchChatList(FetchChatListRef 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) { @@ -94,7 +93,7 @@ Future?> fetchChatList(FetchChatListRef ref) async { activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey; // Decode the chat list from the DHT - IList out = const IListConst([]); + IList out = const IListConst([]); await (await DHTShortArray.openOwned( proto.OwnedDHTRecordPointerProto.fromProto( activeAccountInfo.account.chatList), @@ -105,7 +104,7 @@ Future?> fetchChatList(FetchChatListRef ref) async { if (cir == null) { throw Exception('Failed to get chat'); } - out = out.add(Chat.fromBuffer(cir)); + out = out.add(proto.Chat.fromBuffer(cir)); } }); diff --git a/lib/providers/connection_state.freezed.dart b/lib/providers/connection_state.freezed.dart index 350a1af..9616832 100644 --- a/lib/providers/connection_state.freezed.dart +++ b/lib/providers/connection_state.freezed.dart @@ -116,7 +116,7 @@ class _$ConnectionStateImpl extends _ConnectionState { } @override - bool operator ==(dynamic other) { + bool operator ==(Object other) { return identical(this, other) || (other.runtimeType == runtimeType && other is _$ConnectionStateImpl && diff --git a/lib/providers/contact.dart b/lib/providers/contact.dart index d935e73..c44b302 100644 --- a/lib/providers/contact.dart +++ b/lib/providers/contact.dart @@ -4,7 +4,6 @@ import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; import '../proto/proto.dart' as proto; -import '../proto/proto.dart' show Contact; import '../veilid_support/veilid_support.dart'; import '../tools/tools.dart'; @@ -24,7 +23,7 @@ Future createContact({ activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey; // Create Contact - final contact = Contact() + final contact = proto.Contact() ..editedProfile = profile ..remoteProfile = profile ..identityMasterJson = jsonEncode(remoteIdentity.toJson()) @@ -51,7 +50,7 @@ Future createContact({ Future deleteContact( {required ActiveAccountInfo activeAccountInfo, - required Contact contact}) async { + required proto.Contact contact}) async { final pool = await DHTRecordPool.instance(); final accountRecordKey = activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey; @@ -104,7 +103,7 @@ Future deleteContact( /// Get the active account contact list @riverpod -Future?> fetchContactList(FetchContactListRef ref) async { +Future?> fetchContactList(FetchContactListRef ref) async { // See if we've logged into this account or if it is locked final activeAccountInfo = await ref.watch(fetchActiveAccountProvider.future); if (activeAccountInfo == null) { @@ -114,7 +113,7 @@ Future?> fetchContactList(FetchContactListRef ref) async { activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey; // Decode the contact list from the DHT - IList out = const IListConst([]); + IList out = const IListConst([]); await (await DHTShortArray.openOwned( proto.OwnedDHTRecordPointerProto.fromProto( activeAccountInfo.account.contactList), @@ -125,7 +124,7 @@ Future?> fetchContactList(FetchContactListRef ref) async { if (cir == null) { throw Exception('Failed to get contact'); } - out = out.add(Contact.fromBuffer(cir)); + out = out.add(proto.Contact.fromBuffer(cir)); } }); diff --git a/lib/providers/contact.g.dart b/lib/providers/contact.g.dart index 823f594..b428110 100644 --- a/lib/providers/contact.g.dart +++ b/lib/providers/contact.g.dart @@ -6,14 +6,14 @@ part of 'contact.dart'; // RiverpodGenerator // ************************************************************************** -String _$fetchContactListHash() => r'f75cb33fbc664404bba122f1e128e437e0f0b2da'; +String _$fetchContactListHash() => r'03e5b90435c331be87495d999a62a97af5b74d9e'; /// Get the active account contact list /// /// Copied from [fetchContactList]. @ProviderFor(fetchContactList) final fetchContactListProvider = - AutoDisposeFutureProvider?>.internal( + AutoDisposeFutureProvider?>.internal( fetchContactList, name: r'fetchContactListProvider', debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') @@ -23,6 +23,7 @@ final fetchContactListProvider = allTransitiveDependencies: null, ); -typedef FetchContactListRef = AutoDisposeFutureProviderRef?>; +typedef FetchContactListRef + = AutoDisposeFutureProviderRef?>; // ignore_for_file: type=lint // ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member diff --git a/lib/providers/contact_invite.dart b/lib/providers/contact_invite.dart index 3f7bd72..e776409 100644 --- a/lib/providers/contact_invite.dart +++ b/lib/providers/contact_invite.dart @@ -6,15 +6,6 @@ import 'package:riverpod_annotation/riverpod_annotation.dart'; import '../entities/local_account.dart'; import '../proto/proto.dart' as proto; -import '../proto/proto.dart' - show - ContactInvitation, - ContactInvitationRecord, - ContactRequest, - ContactRequestPrivate, - ContactResponse, - SignedContactInvitation, - SignedContactResponse; import '../tools/tools.dart'; import '../veilid_support/veilid_support.dart'; import 'account.dart'; @@ -48,7 +39,7 @@ class AcceptedOrRejectedContact { Future checkAcceptRejectContact( {required ActiveAccountInfo activeAccountInfo, - required ContactInvitationRecord contactInvitationRecord}) async { + required proto.ContactInvitationRecord contactInvitationRecord}) async { // Open the contact request inbox try { final pool = await DHTRecordPool.instance(); @@ -68,15 +59,17 @@ Future checkAcceptRejectContact( defaultSubkey: 1)) .scope((contactRequestInbox) async { // - final signedContactResponse = await contactRequestInbox - .getProtobuf(SignedContactResponse.fromBuffer, forceRefresh: true); + final signedContactResponse = await contactRequestInbox.getProtobuf( + proto.SignedContactResponse.fromBuffer, + forceRefresh: true); if (signedContactResponse == null) { return null; } final contactResponseBytes = Uint8List.fromList(signedContactResponse.contactResponse); - final contactResponse = ContactResponse.fromBuffer(contactResponseBytes); + final contactResponse = + proto.ContactResponse.fromBuffer(contactResponseBytes); final contactIdentityMasterRecordKey = proto.TypedKeyProto.fromProto( contactResponse.identityMasterRecordKey); final cs = await pool.veilid.getCryptoSystem(recordKey.kind); @@ -154,7 +147,7 @@ Future checkAcceptRejectContact( Future deleteContactInvitation( {required bool accepted, required ActiveAccountInfo activeAccountInfo, - required ContactInvitationRecord contactInvitationRecord}) async { + required proto.ContactInvitationRecord contactInvitationRecord}) async { final pool = await DHTRecordPool.instance(); final accountRecordKey = activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey; @@ -235,7 +228,7 @@ Future createContactInvitation( .deleteScope((localConversation) async { // dont bother reopening localConversation with writer // Make ContactRequestPrivate and encrypt with the writer secret - final crpriv = ContactRequestPrivate() + final crpriv = proto.ContactRequestPrivate() ..writerKey = contactRequestWriter.key.toProto() ..profile = activeAccountInfo.account.profile ..identityMasterRecordKey = @@ -247,7 +240,7 @@ Future createContactInvitation( await cs.encryptAeadWithNonce(crprivbytes, contactRequestWriter.secret); // Create ContactRequest and embed contactrequestprivate - final creq = ContactRequest() + final creq = proto.ContactRequest() ..encryptionKeyType = encryptionKeyType.toProto() ..private = encryptedContactRequestPrivate; @@ -263,18 +256,18 @@ Future createContactInvitation( await contactRequestInbox.eventualWriteProtobuf(creq); // Create ContactInvitation and SignedContactInvitation - final cinv = ContactInvitation() + final cinv = proto.ContactInvitation() ..contactRequestInboxKey = contactRequestInbox.key.toProto() ..writerSecret = encryptedSecret; final cinvbytes = cinv.writeToBuffer(); - final scinv = SignedContactInvitation() + final scinv = proto.SignedContactInvitation() ..contactInvitation = cinvbytes ..identitySignature = (await cs.sign(identityKey, identitySecret, cinvbytes)).toProto(); signedContactInvitationBytes = scinv.writeToBuffer(); // Create ContactInvitationRecord - final cinvrec = ContactInvitationRecord() + final cinvrec = proto.ContactInvitationRecord() ..contactRequestInbox = contactRequestInbox.ownedDHTRecordPointer.toProto() ..writerKey = contactRequestWriter.key.toProto() @@ -311,11 +304,11 @@ class ValidContactInvitation { required this.contactIdentityMaster, required this.writer}); - SignedContactInvitation signedContactInvitation; - ContactInvitation contactInvitation; + proto.SignedContactInvitation signedContactInvitation; + proto.ContactInvitation contactInvitation; TypedKey contactRequestInboxKey; - ContactRequest contactRequest; - ContactRequestPrivate contactRequestPrivate; + proto.ContactRequest contactRequest; + proto.ContactRequestPrivate contactRequestPrivate; IdentityMaster contactIdentityMaster; KeyPair writer; } @@ -327,7 +320,7 @@ typedef GetEncryptionKeyCallback = Future Function( Future validateContactInvitation( {required ActiveAccountInfo activeAccountInfo, - required IList? contactInvitationRecords, + required IList? contactInvitationRecords, required Uint8List inviteData, required GetEncryptionKeyCallback getEncryptionKeyCallback}) async { final accountRecordKey = @@ -434,7 +427,7 @@ Future acceptContactInvitation( remoteIdentityPublicKey: validContactInvitation.contactIdentityMaster .identityPublicTypedKey(), callback: (localConversation) async { - final contactResponse = ContactResponse() + final contactResponse = proto.ContactResponse() ..accept = true ..remoteConversationRecordKey = localConversation.key.toProto() ..identityMasterRecordKey = activeAccountInfo @@ -450,13 +443,14 @@ Future acceptContactInvitation( activeAccountInfo.userLogin.identitySecret.value, contactResponseBytes); - final signedContactResponse = SignedContactResponse() + final signedContactResponse = proto.SignedContactResponse() ..contactResponse = contactResponseBytes ..identitySignature = identitySignature.toProto(); // Write the acceptance to the inbox if (await contactRequestInbox.tryWriteProtobuf( - SignedContactResponse.fromBuffer, signedContactResponse, + proto.SignedContactResponse.fromBuffer, + signedContactResponse, subkey: 1) != null) { throw Exception('failed to accept contact invitation'); @@ -494,7 +488,7 @@ Future rejectContactInvitation(ActiveAccountInfo activeAccountInfo, final cs = await pool.veilid .getCryptoSystem(validContactInvitation.contactRequestInboxKey.kind); - final contactResponse = ContactResponse() + final contactResponse = proto.ContactResponse() ..accept = false ..identityMasterRecordKey = activeAccountInfo .localAccount.identityMaster.masterRecordKey @@ -506,13 +500,13 @@ Future rejectContactInvitation(ActiveAccountInfo activeAccountInfo, activeAccountInfo.userLogin.identitySecret.value, contactResponseBytes); - final signedContactResponse = SignedContactResponse() + final signedContactResponse = proto.SignedContactResponse() ..contactResponse = contactResponseBytes ..identitySignature = identitySignature.toProto(); // Write the rejection to the inbox if (await contactRequestInbox.tryWriteProtobuf( - SignedContactResponse.fromBuffer, signedContactResponse, + proto.SignedContactResponse.fromBuffer, signedContactResponse, subkey: 1) != null) { log.error('failed to reject contact invitation'); @@ -524,7 +518,7 @@ Future rejectContactInvitation(ActiveAccountInfo activeAccountInfo, /// Get the active account contact invitation list @riverpod -Future?> fetchContactInvitationRecords( +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); @@ -535,7 +529,7 @@ Future?> fetchContactInvitationRecords( activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey; // Decode the contact invitation list from the DHT - IList out = const IListConst([]); + IList out = const IListConst([]); try { await (await DHTShortArray.openOwned( @@ -548,7 +542,7 @@ Future?> fetchContactInvitationRecords( if (cir == null) { throw Exception('Failed to get contact invitation record'); } - out = out.add(ContactInvitationRecord.fromBuffer(cir)); + out = out.add(proto.ContactInvitationRecord.fromBuffer(cir)); } }); } on VeilidAPIExceptionTryAgain catch (_) { diff --git a/lib/providers/conversation.dart b/lib/providers/conversation.dart index 7e1c56b..617ead3 100644 --- a/lib/providers/conversation.dart +++ b/lib/providers/conversation.dart @@ -4,7 +4,6 @@ import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; import '../proto/proto.dart' as proto; -import '../proto/proto.dart' show Conversation, Message; import '../tools/tools.dart'; import '../veilid_init.dart'; @@ -79,7 +78,7 @@ Future createConversation( parent: localConversation.key, crypto: crypto, smplWriter: writer)) .deleteScope((messages) async { // Write local conversation key - final conversation = Conversation() + final conversation = proto.Conversation() ..profile = activeAccountInfo.account.profile ..identityMasterJson = jsonEncode(activeAccountInfo.localAccount.identityMaster.toJson()) @@ -87,7 +86,7 @@ Future createConversation( // final update = await localConversation.tryWriteProtobuf( - Conversation.fromBuffer, conversation); + proto.Conversation.fromBuffer, conversation); if (update != null) { throw Exception('Failed to write local conversation'); } @@ -96,7 +95,7 @@ Future createConversation( }); } -Future readRemoteConversation({ +Future readRemoteConversation({ required ActiveAccountInfo activeAccountInfo, required TypedKey remoteConversationRecordKey, required TypedKey remoteIdentityPublicKey, @@ -113,16 +112,16 @@ Future readRemoteConversation({ .scope((remoteConversation) async { // final conversation = - await remoteConversation.getProtobuf(Conversation.fromBuffer); + await remoteConversation.getProtobuf(proto.Conversation.fromBuffer); return conversation; }); } -Future writeLocalConversation({ +Future writeLocalConversation({ required ActiveAccountInfo activeAccountInfo, required TypedKey localConversationRecordKey, required TypedKey remoteIdentityPublicKey, - required Conversation conversation, + required proto.Conversation conversation, }) async { final accountRecordKey = activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey; @@ -138,7 +137,7 @@ Future writeLocalConversation({ .scope((localConversation) async { // final update = await localConversation.tryWriteProtobuf( - Conversation.fromBuffer, conversation); + proto.Conversation.fromBuffer, conversation); if (update != null) { return update; } @@ -146,7 +145,7 @@ Future writeLocalConversation({ }); } -Future readLocalConversation({ +Future readLocalConversation({ required ActiveAccountInfo activeAccountInfo, required TypedKey localConversationRecordKey, required TypedKey remoteIdentityPublicKey, @@ -163,7 +162,8 @@ Future readLocalConversation({ parent: accountRecordKey, crypto: crypto)) .scope((localConversation) async { // - final update = await localConversation.getProtobuf(Conversation.fromBuffer); + final update = + await localConversation.getProtobuf(proto.Conversation.fromBuffer); if (update != null) { return update; } @@ -175,7 +175,7 @@ Future addLocalConversationMessage( {required ActiveAccountInfo activeAccountInfo, required TypedKey localConversationRecordKey, required TypedKey remoteIdentityPublicKey, - required Message message}) async { + required proto.Message message}) async { final conversation = await readLocalConversation( activeAccountInfo: activeAccountInfo, localConversationRecordKey: localConversationRecordKey, @@ -201,7 +201,7 @@ Future mergeLocalConversationMessages( {required ActiveAccountInfo activeAccountInfo, required TypedKey localConversationRecordKey, required TypedKey remoteIdentityPublicKey, - required IList newMessages}) async { + required IList newMessages}) async { final conversation = await readLocalConversation( activeAccountInfo: activeAccountInfo, localConversationRecordKey: localConversationRecordKey, @@ -262,7 +262,7 @@ Future mergeLocalConversationMessages( return changed; } -Future?> getLocalConversationMessages({ +Future?> getLocalConversationMessages({ required ActiveAccountInfo activeAccountInfo, required TypedKey localConversationRecordKey, required TypedKey remoteIdentityPublicKey, @@ -283,9 +283,9 @@ Future?> getLocalConversationMessages({ return (await DHTShortArray.openRead(messagesRecordKey, parent: localConversationRecordKey, crypto: crypto)) .scope((messages) async { - var out = IList(); + var out = IList(); for (var i = 0; i < messages.length; i++) { - final msg = await messages.getItemProtobuf(Message.fromBuffer, i); + final msg = await messages.getItemProtobuf(proto.Message.fromBuffer, i); if (msg == null) { throw Exception('Failed to get message'); } @@ -295,7 +295,7 @@ Future?> getLocalConversationMessages({ }); } -Future?> getRemoteConversationMessages({ +Future?> getRemoteConversationMessages({ required ActiveAccountInfo activeAccountInfo, required TypedKey remoteConversationRecordKey, required TypedKey remoteIdentityPublicKey, @@ -316,9 +316,9 @@ Future?> getRemoteConversationMessages({ return (await DHTShortArray.openRead(messagesRecordKey, parent: remoteConversationRecordKey, crypto: crypto)) .scope((messages) async { - var out = IList(); + var out = IList(); for (var i = 0; i < messages.length; i++) { - final msg = await messages.getItemProtobuf(Message.fromBuffer, i); + final msg = await messages.getItemProtobuf(proto.Message.fromBuffer, i); if (msg == null) { throw Exception('Failed to get message'); } @@ -332,7 +332,7 @@ Future?> getRemoteConversationMessages({ class ActiveConversationMessages extends _$ActiveConversationMessages { /// Get message for active conversation @override - FutureOr?> build() async { + FutureOr?> build() async { await eventualVeilid.future; final activeChat = ref.watch(activeChatStateProvider); diff --git a/lib/veilid_support/dht_support/src/dht_record_pool.freezed.dart b/lib/veilid_support/dht_support/src/dht_record_pool.freezed.dart index a90b480..a00efd3 100644 --- a/lib/veilid_support/dht_support/src/dht_record_pool.freezed.dart +++ b/lib/veilid_support/dht_support/src/dht_record_pool.freezed.dart @@ -156,7 +156,7 @@ class _$DHTRecordPoolAllocationsImpl implements _DHTRecordPoolAllocations { } @override - bool operator ==(dynamic other) { + bool operator ==(Object other) { return identical(this, other) || (other.runtimeType == runtimeType && other is _$DHTRecordPoolAllocationsImpl && @@ -327,7 +327,7 @@ class _$OwnedDHTRecordPointerImpl implements _OwnedDHTRecordPointer { } @override - bool operator ==(dynamic other) { + bool operator ==(Object other) { return identical(this, other) || (other.runtimeType == runtimeType && other is _$OwnedDHTRecordPointerImpl && diff --git a/lib/veilid_support/src/config.dart b/lib/veilid_support/src/config.dart index 3ffa1ca..e45cfe7 100644 --- a/lib/veilid_support/src/config.dart +++ b/lib/veilid_support/src/config.dart @@ -1,3 +1,4 @@ +import 'package:flutter/foundation.dart'; import 'package:veilid/veilid.dart'; Future getVeilidChatConfig() async { @@ -18,8 +19,32 @@ Future getVeilidChatConfig() async { config.copyWith(blockStore: config.blockStore.copyWith(delete: true)); } + // ignore: do_not_use_environment + const envNetwork = String.fromEnvironment('NETWORK'); + if (envNetwork.isNotEmpty) { + final bootstrap = kIsWeb + ? ['ws://bootstrap.$envNetwork.veilid.net:5150/ws'] + : ['bootstrap.$envNetwork.veilid.net']; + config = config.copyWith( + network: config.network.copyWith( + routingTable: + config.network.routingTable.copyWith(bootstrap: bootstrap))); + } + + // ignore: do_not_use_environment + const envBootstrap = String.fromEnvironment('BOOTSTRAP'); + if (envBootstrap.isNotEmpty) { + final bootstrap = envBootstrap.split(',').map((e) => e.trim()).toList(); + config = config.copyWith( + network: config.network.copyWith( + routingTable: + config.network.routingTable.copyWith(bootstrap: bootstrap))); + } + return config.copyWith( - capabilities: const VeilidConfigCapabilities(disable: ['DHTV', 'TUNL']), + capabilities: + // XXX: Remove DHTV and DHTW when we get background sync implemented + const VeilidConfigCapabilities(disable: ['DHTV', 'DHTW', 'TUNL']), protectedStore: config.protectedStore.copyWith(allowInsecureFallback: true), // network: config.network.copyWith( // dht: config.network.dht.copyWith( diff --git a/lib/veilid_support/src/identity.freezed.dart b/lib/veilid_support/src/identity.freezed.dart index d8626df..9948cce 100644 --- a/lib/veilid_support/src/identity.freezed.dart +++ b/lib/veilid_support/src/identity.freezed.dart @@ -126,7 +126,7 @@ class _$AccountRecordInfoImpl implements _AccountRecordInfo { } @override - bool operator ==(dynamic other) { + bool operator ==(Object other) { return identical(this, other) || (other.runtimeType == runtimeType && other is _$AccountRecordInfoImpl && @@ -268,7 +268,7 @@ class _$IdentityImpl implements _Identity { } @override - bool operator ==(dynamic other) { + bool operator ==(Object other) { return identical(this, other) || (other.runtimeType == runtimeType && other is _$IdentityImpl && @@ -503,7 +503,7 @@ class _$IdentityMasterImpl implements _IdentityMaster { } @override - bool operator ==(dynamic other) { + bool operator ==(Object other) { return identical(this, other) || (other.runtimeType == runtimeType && other is _$IdentityMasterImpl && diff --git a/macos/Podfile.lock b/macos/Podfile.lock index e321ce3..949cb69 100644 --- a/macos/Podfile.lock +++ b/macos/Podfile.lock @@ -3,7 +3,7 @@ PODS: - FMDB (2.7.5): - FMDB/standard (= 2.7.5) - FMDB/standard (2.7.5) - - mobile_scanner (3.0.0): + - mobile_scanner (3.5.5): - FlutterMacOS - pasteboard (0.0.1): - FlutterMacOS @@ -76,7 +76,7 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24 FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a - mobile_scanner: ed7618fb749adc6574563e053f3b8e5002c13994 + mobile_scanner: d12930b68bf502497f78b8b5182aeccfaa1e04f6 pasteboard: 9b69dba6fedbb04866be632205d532fe2f6b1d99 path_provider_foundation: 29f094ae23ebbca9d3d0cec13889cd9060c0e943 screen_retriever: 59634572a57080243dd1bf715e55b6c54f241a38 diff --git a/pubspec.lock b/pubspec.lock index d0d040d..ffbd7a1 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -29,26 +29,26 @@ packages: dependency: "direct main" description: name: animated_theme_switcher - sha256: a131266f7021a8a663da4c4848c53c62178949a7517c2af00b22e4c614352302 + sha256: de8ce9872d6e6676ab1140f76ff00cd0084a9dfee62168044062629927949652 url: "https://pub.dev" source: hosted - version: "2.0.8" + version: "2.0.9" ansicolor: dependency: "direct main" description: name: ansicolor - sha256: "607f8fa9786f392043f169898923e6c59b4518242b68b8862eb8a8b7d9c30b4a" + sha256: "8bf17a8ff6ea17499e40a2d2542c2f481cd7615760c6d34065cb22bfd22e6880" url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "2.0.2" archive: dependency: "direct main" description: name: archive - sha256: "20071638cbe4e5964a427cfa0e86dce55d060bc7d82d56f3554095d7239a8765" + sha256: "7b875fd4a20b165a3084bd2d210439b22ebc653f21cea4842729c0c30c82596b" url: "https://pub.dev" source: hosted - version: "3.4.2" + version: "3.4.9" args: dependency: transitive description: @@ -69,10 +69,10 @@ packages: dependency: "direct main" description: name: awesome_extensions - sha256: "6b9c6a5f70d17959ace71d649d3b816b13b73267196035d431ff17e65a228608" + sha256: "79bb5a24f1224795e599c75ec047ca6f4718e17be535544350d213bb37bc88cd" url: "https://pub.dev" source: hosted - version: "2.0.9" + version: "2.0.10" badges: dependency: "direct main" description: @@ -85,10 +85,10 @@ packages: dependency: "direct main" description: name: basic_utils - sha256: "1fb8c5493fc1b9500512b2e153c0b9bcc9e281621cde7f810420f4761be9e38d" + sha256: "2064b21d3c41ed7654bc82cc476fd65542e04d60059b74d5eed490a4da08fc6c" url: "https://pub.dev" source: hosted - version: "5.6.1" + version: "5.7.0" blurry_modal_progress_hud: dependency: "direct main" description: @@ -125,26 +125,26 @@ packages: dependency: transitive description: name: build_daemon - sha256: "5f02d73eb2ba16483e693f80bee4f088563a820e47d1027d4cdfe62b5bb43e65" + sha256: "0343061a33da9c5810b2d6cee51945127d8f4c060b7fbdd9d54917f0a3feaaa1" url: "https://pub.dev" source: hosted - version: "4.0.0" + version: "4.0.1" build_resolvers: dependency: transitive description: name: build_resolvers - sha256: "0713a05b0386bd97f9e63e78108805a4feca5898a4b821d6610857f10c91e975" + sha256: "339086358431fa15d7eca8b6a36e5d783728cf025e559b834f4609a1fcfb7b0a" url: "https://pub.dev" source: hosted - version: "2.4.0" + version: "2.4.2" build_runner: dependency: "direct dev" description: name: build_runner - sha256: "10c6bcdbf9d049a0b666702cf1cee4ddfdc38f02a19d35ae392863b47519848b" + sha256: "67d591d602906ef9201caf93452495ad1812bea2074f04e25dbd7c133785821b" url: "https://pub.dev" source: hosted - version: "2.4.6" + version: "2.4.7" build_runner_core: dependency: transitive description: @@ -165,10 +165,10 @@ packages: dependency: transitive description: name: built_value - sha256: a8de5955205b4d1dbbbc267daddf2178bd737e4bab8987c04a500478c9651e74 + sha256: c9aabae0718ec394e5bc3c7272e6bb0dc0b32201a08fe185ec1d8401d3e39309 url: "https://pub.dev" source: hosted - version: "8.6.3" + version: "8.8.1" cached_network_image: dependency: transitive description: @@ -197,34 +197,34 @@ packages: dependency: transitive description: name: camera - sha256: f63f2687fb1795c36f7c57b18a03071880eabb0fd8b5291b0fcd3fb979cb0fb1 + sha256: "7fa53bb1c2059e58bf86b7ab506e3b2a78e42f82d365b44b013239b975a166ef" url: "https://pub.dev" source: hosted - version: "0.10.5+4" + version: "0.10.5+7" camera_android: dependency: transitive description: name: camera_android - sha256: c978373b41a463c9edda3fea0a06966299f55db63232cd0f0d4efc21a59a0006 + sha256: "7215e38fa0be58cc3203a6e48de3636fb9b1bf93d6eeedf667f882d51b3a4bf3" url: "https://pub.dev" source: hosted - version: "0.10.8+12" + version: "0.10.8+15" camera_avfoundation: dependency: transitive description: name: camera_avfoundation - sha256: dde42d19ad4cdf79287f9e410599db72beaec7e505787dc6abfd0ce5b526e9c0 + sha256: "3c8dd395f18722f01b5f325ddd7f5256e9bcdce538fb9243b378ba759df3283c" url: "https://pub.dev" source: hosted - version: "0.9.13+5" + version: "0.9.13+8" camera_platform_interface: dependency: transitive description: name: camera_platform_interface - sha256: "8734d1c682f034bdb12d0d6ff379b0535a9b8e44266b530025bf8266d6a62f28" + sha256: b6a568984254cadaca41a6b896d87d3b2e79a2e5791afa036f8d524c6783b93a url: "https://pub.dev" source: hosted - version: "2.5.2" + version: "2.7.0" camera_web: dependency: transitive description: @@ -301,10 +301,10 @@ packages: dependency: transitive description: name: cli_util - sha256: b8db3080e59b2503ca9e7922c3df2072cf13992354d5e944074ffa836fba43b7 + sha256: c05b7406fdabc7a49a3929d4af76bcaccbbffcbcdcf185b082e1ae07da323d19 url: "https://pub.dev" source: hosted - version: "0.4.0" + version: "0.4.1" clock: dependency: transitive description: @@ -317,18 +317,18 @@ packages: dependency: transitive description: name: code_builder - sha256: "1be9be30396d7e4c0db42c35ea6ccd7cc6a1e19916b5dc64d6ac216b5544d677" + sha256: feee43a5c05e7b3199bb375a86430b8ada1b04104f2923d0e03cc01ca87b6d84 url: "https://pub.dev" source: hosted - version: "4.7.0" + version: "4.9.0" collection: dependency: transitive description: name: collection - sha256: f092b211a4319e98e5ff58223576de6c2803db36221657b46c82574721240687 + sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a url: "https://pub.dev" source: hosted - version: "1.17.2" + version: "1.18.0" convert: dependency: transitive description: @@ -349,10 +349,10 @@ packages: dependency: transitive description: name: cross_file - sha256: fd832b5384d0d6da4f6df60b854d33accaaeb63aa9e10e736a87381f08dee2cb + sha256: fedaadfa3a6996f75211d835aaeb8fede285dae94262485698afd832371b9a5e url: "https://pub.dev" source: hosted - version: "0.3.3+5" + version: "0.3.3+8" crypto: dependency: transitive description: @@ -381,26 +381,26 @@ packages: dependency: transitive description: name: custom_lint - sha256: "837821e4619c167fd5a547b03bb2fc6be7e65b800ec75528848429705c31ceba" + sha256: "198ec6b8e084d22f508a76556c9afcfb71706ad3f42b083fe0ee923351a96d90" url: "https://pub.dev" source: hosted - version: "0.5.3" + version: "0.5.7" custom_lint_core: dependency: transitive description: name: custom_lint_core - sha256: "3bdebdd52a42b4d6e5be9cd833ad1ecfbbc23e1020ca537060e54085497aea9c" + sha256: f84c3fe2f27ef3b8831953e477e59d4a29c2952623f9eac450d7b40d9cdd94cc url: "https://pub.dev" source: hosted - version: "0.5.3" + version: "0.5.7" dart_style: dependency: transitive description: name: dart_style - sha256: abd7625e16f51f554ea244d090292945ec4d4be7bfbaf2ec8cccea568919d334 + sha256: "40ae61a5d43feea6d24bd22c0537a6629db858963b99b4bc1c3db80676f32368" url: "https://pub.dev" source: hosted - version: "2.3.3" + version: "2.3.4" diffutil_dart: dependency: transitive description: @@ -429,10 +429,10 @@ packages: dependency: "direct main" description: name: fast_immutable_collections - sha256: b4f7d3af6e90a80cf7a3dddd0de3b4a46acb446320795b77b034535c4d267fbe + sha256: "3eb1d7495c70598964add20e10666003fad6e855b108fe684ebcbf8ad0c8e120" url: "https://pub.dev" source: hosted - version: "9.1.5" + version: "9.2.0" ffi: dependency: transitive description: @@ -445,10 +445,10 @@ packages: dependency: transitive description: name: file - sha256: "1b92bec4fc2a72f59a8e15af5f52cd441e4a7860b49499d69dfa817af20e925d" + sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c" url: "https://pub.dev" source: hosted - version: "6.1.4" + version: "7.0.0" file_utils: dependency: transitive description: @@ -474,10 +474,10 @@ packages: dependency: "direct main" description: name: flutter_animate - sha256: "62f346340a96192070e31e3f2a1bd30a28530f1fe8be978821e06cd56b74d6d2" + sha256: "1dbc1aabfb8ec1e9d9feed2b675c21fb6b0a11f99be53ec3bc0f1901af6a8eb7" url: "https://pub.dev" source: hosted - version: "4.2.0+1" + version: "4.3.0" flutter_cache_manager: dependency: transitive description: @@ -498,10 +498,10 @@ packages: dependency: "direct main" description: name: flutter_chat_ui - sha256: d2b7d99fae88d17fdab13f4be3e6ae15c4ceaa5d3e199b61c254a67222d42611 + sha256: "6a4712026429d3b28547bd3d147ded44f8dd53dacc1ff14113693d7b7dd14382" url: "https://pub.dev" source: hosted - version: "1.6.9" + version: "1.6.10" flutter_form_builder: dependency: "direct main" description: @@ -514,10 +514,10 @@ packages: dependency: "direct main" description: name: flutter_hooks - sha256: "6ae13b1145c589112cbd5c4fda6c65908993a9cb18d4f82042e9c28dd9fbf611" + sha256: "7c8db779c2d1010aa7f9ea3fbefe8f86524fcb87b69e8b0af31e1a4b55422dec" url: "https://pub.dev" source: hosted - version: "0.20.1" + version: "0.20.3" flutter_link_previewer: dependency: transitive description: @@ -543,10 +543,10 @@ packages: dependency: "direct main" description: name: flutter_native_splash - sha256: ecff62b3b893f2f665de7e4ad3de89f738941fcfcaaba8ee601e749efafa4698 + sha256: "141b20f15a2c4fe6e33c49257ca1bc114fc5c500b04fcbc8d75016bb86af672f" url: "https://pub.dev" source: hosted - version: "2.3.2" + version: "2.3.8" flutter_parsed_text: dependency: transitive description: @@ -559,26 +559,26 @@ packages: dependency: transitive description: name: flutter_plugin_android_lifecycle - sha256: f185ac890306b5779ecbd611f52502d8d4d63d27703ef73161ca0407e815f02c + sha256: b068ffc46f82a55844acfa4fdbb61fad72fa2aef0905548419d97f0f95c456da url: "https://pub.dev" source: hosted - version: "2.0.16" + version: "2.0.17" flutter_riverpod: dependency: "direct main" description: name: flutter_riverpod - sha256: fcea39b84b666649280f6f678bc0bb479253bf865abc0387a8b11dac6477bf92 + sha256: da9591d1f8d5881628ccd5c25c40e74fc3eef50ba45e40c3905a06e1712412d5 url: "https://pub.dev" source: hosted - version: "2.4.1" + version: "2.4.9" flutter_slidable: dependency: "direct main" description: name: flutter_slidable - sha256: cc4231579e3eae41ae166660df717f4bad1359c87f4a4322ad8ba1befeb3d2be + sha256: "19ed4813003a6ff4e9c6bcce37e792a2a358919d7603b2b31ff200229191e44c" url: "https://pub.dev" source: hosted - version: "3.0.0" + version: "3.0.1" flutter_spinkit: dependency: "direct main" description: @@ -591,10 +591,10 @@ packages: dependency: "direct main" description: name: flutter_svg - sha256: "8c5d68a82add3ca76d792f058b186a0599414f279f00ece4830b9b231b570338" + sha256: d39e7f95621fc84376bc0f7d504f05c3a41488c562f4a8ad410569127507402c url: "https://pub.dev" source: hosted - version: "2.0.7" + version: "2.0.9" flutter_test: dependency: "direct dev" description: flutter @@ -625,10 +625,10 @@ packages: dependency: "direct dev" description: name: freezed - sha256: be7826ed5d87e98c924a839542674fc14edbcb3e4fc0adbc058d680f2b241837 + sha256: "6c5031daae12c7072b3a87eff98983076434b4889ef2a44384d0cae3f82372ba" url: "https://pub.dev" source: hosted - version: "2.4.3" + version: "2.4.6" freezed_annotation: dependency: "direct main" description: @@ -665,10 +665,10 @@ packages: dependency: "direct main" description: name: go_router - sha256: a07c781bf55bf11ae85133338e4850f0b4e33e261c44a66c750fc707d65d8393 + sha256: "2ccd74480706e0a70a0e0dfa9543dede41bc11d0fe3b146a6ad7b7686f6b4407" url: "https://pub.dev" source: hosted - version: "11.1.2" + version: "11.1.4" graphs: dependency: transitive description: @@ -681,10 +681,10 @@ packages: dependency: "direct main" description: name: hooks_riverpod - sha256: a5242fee89736eaf7e5565c271e2d87b0aeb9953ee936de819339366aebc6882 + sha256: c12a456e03ef9be65b0be66963596650ad7a3220e96c7e7b0a048562ea32d6ae url: "https://pub.dev" source: hosted - version: "2.4.1" + version: "2.4.9" html: dependency: transitive description: @@ -697,10 +697,10 @@ packages: dependency: transitive description: name: http - sha256: "759d1a329847dd0f39226c688d3e06a6b8679668e350e2891a6474f8b4bb8525" + sha256: d4872660c46d929f6b8a9ef4e7a7eff7e49bbf0c4ec3f385ee32df5119175139 url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "1.1.2" http_multi_server: dependency: transitive description: @@ -721,10 +721,10 @@ packages: dependency: "direct dev" description: name: icons_launcher - sha256: "69de6373013966ea033f4cefbbbae258ccbfe790a6cfc69796cb33fda996298a" + sha256: "3ed4560181f238e69ca5d55589d6946ef31e6a321c934251a26ce1d9e9867305" url: "https://pub.dev" source: hosted - version: "2.1.4" + version: "2.1.6" image: dependency: "direct main" description: @@ -825,10 +825,10 @@ packages: dependency: transitive description: name: meta - sha256: "3c74dbf8763d36539f114c799d8a2d87343b5067e9d796ca22b5eb8437090ee3" + sha256: a6e590c838b18133bb482a2745ad77c5bb7715fb0451209e1a7567d416678b8e url: "https://pub.dev" source: hosted - version: "1.9.1" + version: "1.10.0" mime: dependency: transitive description: @@ -841,26 +841,26 @@ packages: dependency: "direct main" description: name: mobile_scanner - sha256: "2fbc3914fe625e196c64ea8ffc4084cd36781d2be276d4d5923b11af3b5d44ff" + sha256: c3e5bba1cb626b6ab4fc46610f72a136803f6854267967e19f4a4a6a31ff9b74 url: "https://pub.dev" source: hosted - version: "3.4.1" + version: "3.5.5" motion_toast: dependency: "direct main" description: name: motion_toast - sha256: "5742e33ec2f11210f5269294304fb9bd0f30eace78ad23925eb9306dce7763c9" + sha256: "1bdd11696de9151804644d3dadcbcfaa55749db0353aeca150389ecdeb2eaaac" url: "https://pub.dev" source: hosted - version: "2.7.9" + version: "2.7.10" mutex: dependency: "direct main" description: name: mutex - sha256: "03116a4e46282a671b46c12de649d72c0ed18188ffe12a8d0fc63e83f4ad88f4" + sha256: "8827da25de792088eb33e572115a5eb0d61d61a3c01acbc8bcbe76ed78f1a1f2" url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "3.1.0" octo_image: dependency: transitive description: @@ -913,10 +913,10 @@ packages: dependency: transitive description: name: path_provider_android - sha256: "6b8b19bd80da4f11ce91b2d1fb931f3006911477cec227cce23d3253d80df3f1" + sha256: e595b98692943b4881b219f0a9e3945118d3c16bd7e2813f98ec6e532d905f72 url: "https://pub.dev" source: hosted - version: "2.2.0" + version: "2.2.1" path_provider_foundation: dependency: transitive description: @@ -953,10 +953,10 @@ packages: dependency: transitive description: name: petitparser - sha256: cb3798bef7fc021ac45b308f4b51208a152792445cce0448c9a4ba5879dd8750 + sha256: c15605cd28af66339f8eb6fbe0e541bfe2d1b72d5825efc6598f3e0a31b9ad27 url: "https://pub.dev" source: hosted - version: "5.4.0" + version: "6.0.2" photo_view: dependency: transitive description: @@ -977,10 +977,10 @@ packages: dependency: transitive description: name: platform - sha256: ae68c7bfcd7383af3629daafb32fb4e8681c7154428da4febcff06200585f102 + sha256: "0a279f0707af40c890e80b1e9df8bb761694c074ba7e1d4ab1bc4b728e200b59" url: "https://pub.dev" source: hosted - version: "3.1.2" + version: "3.1.3" platform_info: dependency: transitive description: @@ -993,10 +993,10 @@ packages: dependency: transitive description: name: plugin_platform_interface - sha256: da3fdfeccc4d4ff2da8f8c556704c08f912542c5fb3cf2233ed75372384a034d + sha256: f4f88d4a900933e7267e2b353594774fc0d07fb072b47eedcd5b54e1ea3269f8 url: "https://pub.dev" source: hosted - version: "2.1.6" + version: "2.1.7" pointycastle: dependency: transitive description: @@ -1057,10 +1057,10 @@ packages: dependency: "direct main" description: name: qr_code_dart_scan - sha256: "4b5222c044700f9ecb3d1c39ca9c5cf433b508f81a0649b768628d3b5ee2ffc4" + sha256: ab40c5e8cf09e723fdd1c24ee23ae7187e44d958cc4b1554b4cd094845ae6989 url: "https://pub.dev" source: hosted - version: "0.7.2" + version: "0.7.3" qr_flutter: dependency: "direct main" description: @@ -1097,42 +1097,42 @@ packages: dependency: "direct main" description: name: reorderable_grid - sha256: a1322139ec59134e2180acb1b84fe436ea927ce2712ae01da511614131a07d85 + sha256: "0b9cd95ef0f070ef99f92affe9cf85a4aa127099cd1334e5940950ce58cd981d" url: "https://pub.dev" source: hosted - version: "1.0.8" + version: "1.0.10" riverpod: dependency: transitive description: name: riverpod - sha256: ff676bd8a715c7085692fe4919564f78fb90d33b10a1c5c14e740581857cc914 + sha256: "942999ee48b899f8a46a860f1e13cee36f2f77609eb54c5b7a669bb20d550b11" url: "https://pub.dev" source: hosted - version: "2.4.1" + version: "2.4.9" riverpod_analyzer_utils: dependency: transitive description: name: riverpod_analyzer_utils - sha256: d72d7096964baf288b55619fe48100001fc4564ab7923ed0a7f5c7650e03c0d6 + sha256: d4dabc35358413bf4611fcb6abb46308a67c4ef4cd5e69fd3367b11925c59f57 url: "https://pub.dev" source: hosted - version: "0.3.4" + version: "0.5.0" riverpod_annotation: dependency: "direct main" description: name: riverpod_annotation - sha256: aeeb1eb6ccf2d779f2ef730e6d96d560316b677662222316779a8cf0a94ee317 + sha256: b70e95fbd5ca7ce42f5148092022971bb2e9843b6ab71e97d479e8ab52e98979 url: "https://pub.dev" source: hosted - version: "2.1.6" + version: "2.3.3" riverpod_generator: dependency: "direct dev" description: name: riverpod_generator - sha256: "5b36ad2f2b562cffb37212e8d59390b25499bf045b732276e30a207b16a25f61" + sha256: ff8f064f1d7ef3cc6af481bba8e9a3fcdb4d34df34fac1b39bbc003167065be0 url: "https://pub.dev" source: hosted - version: "2.3.3" + version: "2.3.9" rxdart: dependency: transitive description: @@ -1161,34 +1161,34 @@ packages: dependency: "direct main" description: name: searchable_listview - sha256: e1ba75eda1460c24648e54c543843a7142811ea4966c2106e0cc6792128b7127 + sha256: "492bb75e133d1be902d2c1e8aa362f21127260106557492993432a4f5489494a" url: "https://pub.dev" source: hosted - version: "2.7.1" + version: "2.9.1" share_plus: dependency: "direct main" description: name: share_plus - sha256: "6cec740fa0943a826951223e76218df002804adb588235a8910dc3d6b0654e11" + sha256: f74fc3f1cbd99f39760182e176802f693fa0ec9625c045561cfad54681ea93dd url: "https://pub.dev" source: hosted - version: "7.1.0" + version: "7.2.1" share_plus_platform_interface: dependency: transitive description: name: share_plus_platform_interface - sha256: "357412af4178d8e11d14f41723f80f12caea54cf0d5cd29af9dcdab85d58aea7" + sha256: df08bc3a07d01f5ea47b45d03ffcba1fa9cd5370fb44b3f38c70e42cced0f956 url: "https://pub.dev" source: hosted - version: "3.3.0" + version: "3.3.1" shared_preferences: dependency: "direct main" description: name: shared_preferences - sha256: b7f41bad7e521d205998772545de63ff4e6c97714775902c199353f8bf1511ac + sha256: "81429e4481e1ccfb51ede496e916348668fd0921627779233bd24cc3ff6abd02" url: "https://pub.dev" source: hosted - version: "2.2.1" + version: "2.2.2" shared_preferences_android: dependency: transitive description: @@ -1209,10 +1209,10 @@ packages: dependency: transitive description: name: shared_preferences_linux - sha256: c2eb5bf57a2fe9ad6988121609e47d3e07bb3bdca5b6f8444e4cf302428a128a + sha256: "9f2cbcf46d4270ea8be39fa156d86379077c8a5228d9dfdb1164ae0bb93f1faa" url: "https://pub.dev" source: hosted - version: "2.3.1" + version: "2.3.2" shared_preferences_platform_interface: dependency: transitive description: @@ -1225,18 +1225,18 @@ packages: dependency: transitive description: name: shared_preferences_web - sha256: d762709c2bbe80626ecc819143013cc820fa49ca5e363620ee20a8b15a3e3daf + sha256: "7b15ffb9387ea3e237bb7a66b8a23d2147663d391cafc5c8f37b2e7b4bde5d21" url: "https://pub.dev" source: hosted - version: "2.2.1" + version: "2.2.2" shared_preferences_windows: dependency: transitive description: name: shared_preferences_windows - sha256: f763a101313bd3be87edffe0560037500967de9c394a714cd598d945517f694f + sha256: "841ad54f3c8381c480d0c9b508b89a34036f512482c407e6df7a9c4aa2ef8f59" url: "https://pub.dev" source: hosted - version: "2.3.1" + version: "2.3.2" shelf: dependency: transitive description: @@ -1278,10 +1278,10 @@ packages: dependency: transitive description: name: source_gen - sha256: fc0da689e5302edb6177fdd964efcb7f58912f43c28c2047a808f5bfff643d16 + sha256: "14658ba5f669685cd3d63701d01b31ea748310f7ab854e471962670abcf57832" url: "https://pub.dev" source: hosted - version: "1.4.0" + version: "1.5.0" source_helper: dependency: transitive description: @@ -1318,18 +1318,18 @@ packages: dependency: transitive description: name: sqflite_common - sha256: "1b92f368f44b0dee2425bb861cfa17b6f6cf3961f762ff6f941d20b33355660a" + sha256: bb4738f15b23352822f4c42a531677e5c6f522e079461fd240ead29d8d8a54a6 url: "https://pub.dev" source: hosted - version: "2.5.0" + version: "2.5.0+2" stack_trace: dependency: transitive description: name: stack_trace - sha256: c3c7d8edb15bee7f0f74debd4b9c5f3c2ea86766fe4178eb2a18eb30a0bdaed5 + sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" url: "https://pub.dev" source: hosted - version: "1.11.0" + version: "1.11.1" state_notifier: dependency: transitive description: @@ -1342,10 +1342,10 @@ packages: dependency: transitive description: name: stream_channel - sha256: "83615bee9045c1d322bbbd1ba209b7a749c2cbcdcb3fdd1df8eb488b3279c1c8" + sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.1.2" stream_transform: dependency: transitive description: @@ -1374,10 +1374,10 @@ packages: dependency: transitive description: name: synchronized - sha256: "5fcbd27688af6082f5abd611af56ee575342c30e87541d0245f7ff99faa02c60" + sha256: "539ef412b170d65ecdafd780f924e5be3f60032a1128df156adad6c5b373d558" url: "https://pub.dev" source: hosted - version: "3.1.0" + version: "3.1.0+1" system_info2: dependency: transitive description: @@ -1406,10 +1406,10 @@ packages: dependency: transitive description: name: test_api - sha256: "75760ffd7786fffdfb9597c35c5b27eaeec82be8edfb6d71d32651128ed7aab8" + sha256: "5c2f730018264d276c20e4f1503fd1308dfbbae39ec8ee63c5236311ac06954b" url: "https://pub.dev" source: hosted - version: "0.6.0" + version: "0.6.1" timing: dependency: transitive description: @@ -1446,66 +1446,66 @@ packages: dependency: transitive description: name: url_launcher - sha256: "47e208a6711459d813ba18af120d9663c20bdf6985d6ad39fe165d2538378d27" + sha256: e9aa5ea75c84cf46b3db4eea212523591211c3cf2e13099ee4ec147f54201c86 url: "https://pub.dev" source: hosted - version: "6.1.14" + version: "6.2.2" url_launcher_android: dependency: transitive description: name: url_launcher_android - sha256: b04af59516ab45762b2ca6da40fa830d72d0f6045cd97744450b73493fa76330 + sha256: "31222ffb0063171b526d3e569079cf1f8b294075ba323443fdc690842bfd4def" url: "https://pub.dev" source: hosted - version: "6.1.0" + version: "6.2.0" url_launcher_ios: dependency: transitive description: name: url_launcher_ios - sha256: "7c65021d5dee51813d652357bc65b8dd4a6177082a9966bc8ba6ee477baa795f" + sha256: bba3373219b7abb6b5e0d071b0fe66dfbe005d07517a68e38d4fc3638f35c6d3 url: "https://pub.dev" source: hosted - version: "6.1.5" + version: "6.2.1" url_launcher_linux: dependency: transitive description: name: url_launcher_linux - sha256: b651aad005e0cb06a01dbd84b428a301916dc75f0e7ea6165f80057fee2d8e8e + sha256: ab360eb661f8879369acac07b6bb3ff09d9471155357da8443fd5d3cf7363811 url: "https://pub.dev" source: hosted - version: "3.0.6" + version: "3.1.1" url_launcher_macos: dependency: transitive description: name: url_launcher_macos - sha256: b55486791f666e62e0e8ff825e58a023fd6b1f71c49926483f1128d3bbd8fe88 + sha256: b7244901ea3cf489c5335bdacda07264a6e960b1c1b1a9f91e4bc371d9e68234 url: "https://pub.dev" source: hosted - version: "3.0.7" + version: "3.1.0" url_launcher_platform_interface: dependency: transitive description: name: url_launcher_platform_interface - sha256: "95465b39f83bfe95fcb9d174829d6476216f2d548b79c38ab2506e0458787618" + sha256: "980e8d9af422f477be6948bdfb68df8433be71f5743a188968b0c1b887807e50" url: "https://pub.dev" source: hosted - version: "2.1.5" + version: "2.2.0" url_launcher_web: dependency: transitive description: name: url_launcher_web - sha256: "2942294a500b4fa0b918685aff406773ba0a4cd34b7f42198742a94083020ce5" + sha256: "7286aec002c8feecc338cc33269e96b73955ab227456e9fb2a91f7fab8a358e9" url: "https://pub.dev" source: hosted - version: "2.0.20" + version: "2.2.2" url_launcher_windows: dependency: transitive description: name: url_launcher_windows - sha256: "95fef3129dc7cfaba2bc3d5ba2e16063bb561fc6d78e63eee16162bc70029069" + sha256: ecf9725510600aa2bb6d7ddabe16357691b6d2805f66216a97d1b881e21beff7 url: "https://pub.dev" source: hosted - version: "3.0.8" + version: "3.1.1" uuid: dependency: "direct main" description: @@ -1518,26 +1518,26 @@ packages: dependency: transitive description: name: vector_graphics - sha256: "670f6e07aca990b4a2bcdc08a784193c4ccdd1932620244c3a86bb72a0eac67f" + sha256: "0f0c746dd2d6254a0057218ff980fc7f5670fd0fcf5e4db38a490d31eed4ad43" url: "https://pub.dev" source: hosted - version: "1.1.7" + version: "1.1.9+1" vector_graphics_codec: dependency: transitive description: name: vector_graphics_codec - sha256: "7451721781d967db9933b63f5733b1c4533022c0ba373a01bdd79d1a5457f69f" + sha256: "0edf6d630d1bfd5589114138ed8fada3234deacc37966bec033d3047c29248b7" url: "https://pub.dev" source: hosted - version: "1.1.7" + version: "1.1.9+1" vector_graphics_compiler: dependency: transitive description: name: vector_graphics_compiler - sha256: "80a13c613c8bde758b1464a1755a7b3a8f2b6cec61fbf0f5a53c94c30f03ba2e" + sha256: d24333727332d9bd20990f1483af4e09abdb9b1fc7c3db940b56ab5c42790c26 url: "https://pub.dev" source: hosted - version: "1.1.7" + version: "1.1.9+1" vector_math: dependency: transitive description: @@ -1552,7 +1552,7 @@ packages: path: "../veilid/veilid-flutter" relative: true source: path - version: "0.2.4" + version: "0.2.5" visibility_detector: dependency: transitive description: @@ -1573,10 +1573,10 @@ packages: dependency: transitive description: name: web - sha256: dc8ccd225a2005c1be616fe02951e2e342092edf968cf0844220383757ef8f10 + sha256: afe077240a270dcfd2aafe77602b4113645af95d0ad31128cc02bce5ac5d5152 url: "https://pub.dev" source: hosted - version: "0.1.4-beta" + version: "0.3.0" web_socket_channel: dependency: transitive description: @@ -1589,18 +1589,18 @@ packages: dependency: transitive description: name: win32 - sha256: "350a11abd2d1d97e0cc7a28a81b781c08002aa2864d9e3f192ca0ffa18b06ed3" + sha256: b0f37db61ba2f2e9b7a78a1caece0052564d1bc70668156cf3a29d676fe4e574 url: "https://pub.dev" source: hosted - version: "5.0.9" + version: "5.1.1" window_manager: dependency: "direct main" description: name: window_manager - sha256: "6ee795be9124f90660ea9d05e581a466de19e1c89ee74fc4bf528f60c8600edd" + sha256: dcc865277f26a7dad263a47d0e405d77e21f12cb71f30333a52710a408690bd7 url: "https://pub.dev" source: hosted - version: "0.3.6" + version: "0.3.7" xdg_directories: dependency: transitive description: @@ -1613,10 +1613,10 @@ packages: dependency: transitive description: name: xml - sha256: "5bc72e1e45e941d825fd7468b9b4cc3b9327942649aeb6fc5cdbf135f0a86e84" + sha256: b015a8ad1c488f66851d762d3090a21c600e479dc75e68328c52774040cf9226 url: "https://pub.dev" source: hosted - version: "6.3.0" + version: "6.5.0" xterm: dependency: "direct main" description: @@ -1637,10 +1637,10 @@ packages: dependency: "direct main" description: name: zxing2 - sha256: "1e141568c9646bc262fa75aacf739bc151ef6ad0226997c0016cc3da358a1bbc" + sha256: a042961441bd400f59595f9125ef5fca4c888daf0ea59c17f41e0e151f8a12b5 url: "https://pub.dev" source: hosted - version: "0.2.0" + version: "0.2.1" zxing_lib: dependency: transitive description: @@ -1650,5 +1650,5 @@ packages: source: hosted version: "0.9.0" sdks: - dart: ">=3.1.0 <4.0.0" - flutter: ">=3.13.0" + dart: ">=3.2.0 <4.0.0" + flutter: ">=3.16.0" diff --git a/pubspec.yaml b/pubspec.yaml index d498631..6a58ced 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -47,7 +47,7 @@ dependencies: intl: ^0.18.0 json_annotation: ^4.8.1 loggy: ^2.0.3 - mobile_scanner: ^3.4.1 + mobile_scanner: ^3.5.1 motion_toast: ^2.7.8 mutex: ^3.0.1 pasteboard: ^0.2.0 From e8980743875104b8fd0313ca5380f4aa8407e78d Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Thu, 21 Dec 2023 12:10:54 -0500 Subject: [PATCH 02/68] refactoring --- lib/managers/contact_list_manager.dart | 0 lib/managers/valid_contact_invitation.dart | 1 + lib/providers/account.dart | 38 +- lib/providers/account.g.dart | 91 +-- lib/providers/chat.g.dart | 7 +- .../contact_invitation_list_manager.dart | 583 ++++++++++++++++++ .../contact_invitation_list_manager.g.dart | 202 ++++++ lib/providers/contact_invite.dart | 503 --------------- lib/providers/contact_invite.g.dart | 6 +- lib/providers/conversation.dart | 483 +++++++-------- lib/providers/conversation.g.dart | 6 +- lib/providers/local_accounts.dart | 2 +- 12 files changed, 1100 insertions(+), 822 deletions(-) create mode 100644 lib/managers/contact_list_manager.dart create mode 100644 lib/managers/valid_contact_invitation.dart create mode 100644 lib/providers/contact_invitation_list_manager.dart create mode 100644 lib/providers/contact_invitation_list_manager.g.dart diff --git a/lib/managers/contact_list_manager.dart b/lib/managers/contact_list_manager.dart new file mode 100644 index 0000000..e69de29 diff --git a/lib/managers/valid_contact_invitation.dart b/lib/managers/valid_contact_invitation.dart new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/lib/managers/valid_contact_invitation.dart @@ -0,0 +1 @@ + diff --git a/lib/providers/account.dart b/lib/providers/account.dart index 22e532f..93f1ce9 100644 --- a/lib/providers/account.dart +++ b/lib/providers/account.dart @@ -1,10 +1,10 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; import '../entities/local_account.dart'; -import '../proto/proto.dart' as proto; import '../entities/user_login.dart'; +import '../proto/proto.dart' as proto; import '../veilid_support/veilid_support.dart'; - import 'local_accounts.dart'; import 'logins.dart'; @@ -17,22 +17,23 @@ enum AccountInfoStatus { accountReady, } +@immutable class AccountInfo { - AccountInfo({ + const AccountInfo({ required this.status, required this.active, this.account, }); - AccountInfoStatus status; - bool active; - proto.Account? account; + final AccountInfoStatus status; + final bool active; + final proto.Account? account; } /// Get an account from the identity key and if it is logged in and we /// have its secret available, return the account record contents @riverpod -Future fetchAccount(FetchAccountRef ref, +Future fetchAccountInfo(FetchAccountInfoRef ref, {required TypedKey accountMasterRecordKey}) async { // Get which local account we want to fetch the profile for final localAccount = await ref.watch( @@ -40,7 +41,8 @@ Future fetchAccount(FetchAccountRef ref, .future); if (localAccount == null) { // Local account does not exist - return AccountInfo(status: AccountInfoStatus.noAccount, active: false); + return const AccountInfo( + status: AccountInfoStatus.noAccount, active: false); } // See if we've logged into this account or if it is locked @@ -74,21 +76,31 @@ Future fetchAccount(FetchAccountRef ref, status: AccountInfoStatus.accountReady, active: active, account: account); } +@immutable class ActiveAccountInfo { - ActiveAccountInfo({ + const ActiveAccountInfo({ required this.localAccount, required this.userLogin, required this.account, }); + // - LocalAccount localAccount; - UserLogin userLogin; - proto.Account account; + KeyPair getConversationWriter() { + final identityKey = localAccount.identityMaster.identityPublicKey; + final identitySecret = userLogin.identitySecret; + return KeyPair(key: identityKey, secret: identitySecret.value); + } + + // + final LocalAccount localAccount; + final UserLogin userLogin; + final proto.Account account; } /// Get the active account info @riverpod -Future fetchActiveAccount(FetchActiveAccountRef ref) async { +Future fetchActiveAccountInfo( + FetchActiveAccountInfoRef 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)); diff --git a/lib/providers/account.g.dart b/lib/providers/account.g.dart index 6fba2b3..c1b1d59 100644 --- a/lib/providers/account.g.dart +++ b/lib/providers/account.g.dart @@ -6,7 +6,7 @@ part of 'account.dart'; // RiverpodGenerator // ************************************************************************** -String _$fetchAccountHash() => r'f3072fdd89611b53cd9821613acab450b3c08820'; +String _$fetchAccountInfoHash() => r'3d2e3b3ddce5158d03bceaf82cdb35bae000280c'; /// Copied from Dart SDK class _SystemHash { @@ -32,36 +32,36 @@ class _SystemHash { /// Get an account from the identity key and if it is logged in and we /// have its secret available, return the account record contents /// -/// Copied from [fetchAccount]. -@ProviderFor(fetchAccount) -const fetchAccountProvider = FetchAccountFamily(); +/// Copied from [fetchAccountInfo]. +@ProviderFor(fetchAccountInfo) +const fetchAccountInfoProvider = FetchAccountInfoFamily(); /// Get an account from the identity key and if it is logged in and we /// have its secret available, return the account record contents /// -/// Copied from [fetchAccount]. -class FetchAccountFamily extends Family> { +/// Copied from [fetchAccountInfo]. +class FetchAccountInfoFamily extends Family> { /// Get an account from the identity key and if it is logged in and we /// have its secret available, return the account record contents /// - /// Copied from [fetchAccount]. - const FetchAccountFamily(); + /// Copied from [fetchAccountInfo]. + const FetchAccountInfoFamily(); /// Get an account from the identity key and if it is logged in and we /// have its secret available, return the account record contents /// - /// Copied from [fetchAccount]. - FetchAccountProvider call({ + /// Copied from [fetchAccountInfo]. + FetchAccountInfoProvider call({ required Typed accountMasterRecordKey, }) { - return FetchAccountProvider( + return FetchAccountInfoProvider( accountMasterRecordKey: accountMasterRecordKey, ); } @override - FetchAccountProvider getProviderOverride( - covariant FetchAccountProvider provider, + FetchAccountInfoProvider getProviderOverride( + covariant FetchAccountInfoProvider provider, ) { return call( accountMasterRecordKey: provider.accountMasterRecordKey, @@ -80,38 +80,38 @@ class FetchAccountFamily extends Family> { _allTransitiveDependencies; @override - String? get name => r'fetchAccountProvider'; + String? get name => r'fetchAccountInfoProvider'; } /// Get an account from the identity key and if it is logged in and we /// have its secret available, return the account record contents /// -/// Copied from [fetchAccount]. -class FetchAccountProvider extends AutoDisposeFutureProvider { +/// Copied from [fetchAccountInfo]. +class FetchAccountInfoProvider extends AutoDisposeFutureProvider { /// Get an account from the identity key and if it is logged in and we /// have its secret available, return the account record contents /// - /// Copied from [fetchAccount]. - FetchAccountProvider({ + /// Copied from [fetchAccountInfo]. + FetchAccountInfoProvider({ required Typed accountMasterRecordKey, }) : this._internal( - (ref) => fetchAccount( - ref as FetchAccountRef, + (ref) => fetchAccountInfo( + ref as FetchAccountInfoRef, accountMasterRecordKey: accountMasterRecordKey, ), - from: fetchAccountProvider, - name: r'fetchAccountProvider', + from: fetchAccountInfoProvider, + name: r'fetchAccountInfoProvider', debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') ? null - : _$fetchAccountHash, - dependencies: FetchAccountFamily._dependencies, + : _$fetchAccountInfoHash, + dependencies: FetchAccountInfoFamily._dependencies, allTransitiveDependencies: - FetchAccountFamily._allTransitiveDependencies, + FetchAccountInfoFamily._allTransitiveDependencies, accountMasterRecordKey: accountMasterRecordKey, ); - FetchAccountProvider._internal( + FetchAccountInfoProvider._internal( super._createNotifier, { required super.name, required super.dependencies, @@ -125,12 +125,12 @@ class FetchAccountProvider extends AutoDisposeFutureProvider { @override Override overrideWith( - FutureOr Function(FetchAccountRef provider) create, + FutureOr Function(FetchAccountInfoRef provider) create, ) { return ProviderOverride( origin: this, - override: FetchAccountProvider._internal( - (ref) => create(ref as FetchAccountRef), + override: FetchAccountInfoProvider._internal( + (ref) => create(ref as FetchAccountInfoRef), from: from, name: null, dependencies: null, @@ -143,12 +143,12 @@ class FetchAccountProvider extends AutoDisposeFutureProvider { @override AutoDisposeFutureProviderElement createElement() { - return _FetchAccountProviderElement(this); + return _FetchAccountInfoProviderElement(this); } @override bool operator ==(Object other) { - return other is FetchAccountProvider && + return other is FetchAccountInfoProvider && other.accountMasterRecordKey == accountMasterRecordKey; } @@ -161,39 +161,40 @@ class FetchAccountProvider extends AutoDisposeFutureProvider { } } -mixin FetchAccountRef on AutoDisposeFutureProviderRef { +mixin FetchAccountInfoRef on AutoDisposeFutureProviderRef { /// The parameter `accountMasterRecordKey` of this provider. Typed get accountMasterRecordKey; } -class _FetchAccountProviderElement - extends AutoDisposeFutureProviderElement with FetchAccountRef { - _FetchAccountProviderElement(super.provider); +class _FetchAccountInfoProviderElement + extends AutoDisposeFutureProviderElement + with FetchAccountInfoRef { + _FetchAccountInfoProviderElement(super.provider); @override Typed get accountMasterRecordKey => - (origin as FetchAccountProvider).accountMasterRecordKey; + (origin as FetchAccountInfoProvider).accountMasterRecordKey; } -String _$fetchActiveAccountHash() => - r'197e5dd793563ff1d9927309a5ec9db1c9f67f07'; +String _$fetchActiveAccountInfoHash() => + r'85276ff85b0e82c8d3c6313250954f5b578697d1'; /// Get the active account info /// -/// Copied from [fetchActiveAccount]. -@ProviderFor(fetchActiveAccount) -final fetchActiveAccountProvider = +/// Copied from [fetchActiveAccountInfo]. +@ProviderFor(fetchActiveAccountInfo) +final fetchActiveAccountInfoProvider = AutoDisposeFutureProvider.internal( - fetchActiveAccount, - name: r'fetchActiveAccountProvider', + fetchActiveAccountInfo, + name: r'fetchActiveAccountInfoProvider', debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') ? null - : _$fetchActiveAccountHash, + : _$fetchActiveAccountInfoHash, dependencies: null, allTransitiveDependencies: null, ); -typedef FetchActiveAccountRef +typedef FetchActiveAccountInfoRef = AutoDisposeFutureProviderRef; // ignore_for_file: type=lint // ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member diff --git a/lib/providers/chat.g.dart b/lib/providers/chat.g.dart index 411eae1..3f2c8e8 100644 --- a/lib/providers/chat.g.dart +++ b/lib/providers/chat.g.dart @@ -6,13 +6,14 @@ part of 'chat.dart'; // RiverpodGenerator // ************************************************************************** -String _$fetchChatListHash() => r'407692f9d6794a5a2b356d7a34240624b211daa8'; +String _$fetchChatListHash() => r'0c166082625799862128dff09d9286f64785ba6c'; /// Get the active account contact list /// /// Copied from [fetchChatList]. @ProviderFor(fetchChatList) -final fetchChatListProvider = AutoDisposeFutureProvider?>.internal( +final fetchChatListProvider = + AutoDisposeFutureProvider?>.internal( fetchChatList, name: r'fetchChatListProvider', debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') @@ -22,6 +23,6 @@ final fetchChatListProvider = AutoDisposeFutureProvider?>.internal( allTransitiveDependencies: null, ); -typedef FetchChatListRef = AutoDisposeFutureProviderRef?>; +typedef FetchChatListRef = AutoDisposeFutureProviderRef?>; // ignore_for_file: type=lint // ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member diff --git a/lib/providers/contact_invitation_list_manager.dart b/lib/providers/contact_invitation_list_manager.dart new file mode 100644 index 0000000..2058832 --- /dev/null +++ b/lib/providers/contact_invitation_list_manager.dart @@ -0,0 +1,583 @@ +import 'package:fast_immutable_collections/fast_immutable_collections.dart'; +import 'package:fixnum/fixnum.dart'; +import 'package:flutter/foundation.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; +import 'package:mutex/mutex.dart'; + +import '../entities/entities.dart'; +import '../proto/proto.dart' as proto; +import '../tools/tools.dart'; +import '../veilid_support/veilid_support.dart'; +import 'account.dart'; + +part 'contact_invitation_list_manager.g.dart'; + +////////////////////////////////////////////////// + +class ContactInviteInvalidKeyException implements Exception { + const ContactInviteInvalidKeyException(this.type) : super(); + final EncryptionKeyType type; +} + +typedef GetEncryptionKeyCallback = Future Function( + VeilidCryptoSystem cs, + EncryptionKeyType encryptionKeyType, + Uint8List encryptedSecret); + +////////////////////////////////////////////////// +@immutable +class AcceptedContact { + const AcceptedContact({ + required this.profile, + required this.remoteIdentity, + required this.remoteConversationRecordKey, + required this.localConversationRecordKey, + }); + + final proto.Profile profile; + final IdentityMaster remoteIdentity; + final TypedKey remoteConversationRecordKey; + final TypedKey localConversationRecordKey; +} + +@immutable +class InvitationStatus { + const InvitationStatus({required this.acceptedContact}); + final AcceptedContact? acceptedContact; +} + +////////////////////////////////////////////////// + +////////////////////////////////////////////////// +// Mutable state for per-account contact invitations +@riverpod +class ContactInvitationListManager extends _$ContactInvitationListManager { + ContactInvitationListManager._({ + required ActiveAccountInfo activeAccountInfo, + required DHTShortArray dhtRecord, + }) : _activeAccountInfo = activeAccountInfo, + _dhtRecord = dhtRecord, + _records = IList(); + + @override + FutureOr> build( + ActiveAccountInfo activeAccountInfo) async { + // Load initial todo list from the remote repository + ref.onDispose xxxx call close and pass dhtrecord through... could use a context object + and a DHTValueChangeProvider that we watch in build that updates when dht records change + return _open(activeAccountInfo); + } + + static Future _open( + ActiveAccountInfo activeAccountInfo) async { + final accountRecordKey = + activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey; + + final dhtRecord = await DHTShortArray.openOwned( + proto.OwnedDHTRecordPointerProto.fromProto( + activeAccountInfo.account.contactInvitationRecords), + parent: accountRecordKey); + + return ContactInvitationListManager._( + activeAccountInfo: activeAccountInfo, dhtRecord: dhtRecord); + } + + Future close() async { + state = ""; + await _dhtRecord.close(); + } + + Future refresh() async { + for (var i = 0; i < _dhtRecord.length; i++) { + final cir = await _dhtRecord.getItem(i); + if (cir == null) { + throw Exception('Failed to get contact invitation record'); + } + _records = _records.add(proto.ContactInvitationRecord.fromBuffer(cir)); + } + } + + Future createInvitation( + {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 contactRequestWriter = await cs.generateKeyPair(); + final conversationWriter = _activeAccountInfo.getConversationWriter(); + + // Encrypt the writer secret with the encryption key + final encryptedSecret = await encryptSecretToBytes( + secret: contactRequestWriter.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, + schema: DHTSchema.smpl(oCnt: 0, members: [ + DHTSchemaMember(mKey: conversationWriter.key, mCnt: 1) + ]))) + .deleteScope((localConversation) async { + // dont bother reopening localConversation with writer + // Make ContactRequestPrivate and encrypt with the writer secret + final crpriv = proto.ContactRequestPrivate() + ..writerKey = contactRequestWriter.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.encryptAeadWithNonce( + crprivbytes, contactRequestWriter.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: contactRequestWriter.key) + ]), + crypto: const DHTRecordCryptoPublic())) + .deleteScope((contactRequestInbox) async { + // Store ContactRequest in owner subkey + await contactRequestInbox.eventualWriteProtobuf(creq); + + // Create ContactInvitation and SignedContactInvitation + final cinv = proto.ContactInvitation() + ..contactRequestInboxKey = contactRequestInbox.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() + ..contactRequestInbox = + contactRequestInbox.ownedDHTRecordPointer.toProto() + ..writerKey = contactRequestWriter.key.toProto() + ..writerSecret = contactRequestWriter.secret.toProto() + ..localConversationRecordKey = localConversation.key.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 Exception('Failed to add contact invitation record'); + } + }); + }); + }); + + return signedContactInvitationBytes; + } + + Future deleteInvitation( + {required bool accepted, + required proto.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 Exception('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.openRead( + proto.TypedKeyProto.fromProto( + contactInvitationRecord.localConversationRecordKey), + parent: accountRecordKey)) + .delete(); + } + }); + } + + Future validateInvitation( + {required Uint8List inviteData, + required GetEncryptionKeyCallback getEncryptionKeyCallback}) async { + final accountRecordKey = + _activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey; + + 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); + + ValidContactInvitation? out; + + final pool = await DHTRecordPool.instance(); + final cs = await pool.veilid.getCryptoSystem(contactRequestInboxKey.kind); + + // See if we're chatting to ourselves, if so, don't delete it here + final isSelf = _records.indexWhere((cir) => + proto.TypedKeyProto.fromProto(cir.contactRequestInbox.recordKey) == + contactRequestInboxKey) != + -1; + + await (await pool.openRead(contactRequestInboxKey, + parent: accountRecordKey)) + .maybeDeleteScope(!isSelf, (contactRequestInbox) async { + // + final contactRequest = await contactRequestInbox + .getProtobuf(proto.ContactRequest.fromBuffer); + + // Decrypt contact request private + final encryptionKeyType = + EncryptionKeyType.fromProto(contactRequest!.encryptionKeyType); + late final SharedSecret? writerSecret; + try { + writerSecret = await getEncryptionKeyCallback(cs, encryptionKeyType, + Uint8List.fromList(contactInvitation.writerSecret)); + } on Exception catch (_) { + throw ContactInviteInvalidKeyException(encryptionKeyType); + } + if (writerSecret == null) { + return null; + } + + final contactRequestPrivateBytes = await cs.decryptAeadWithNonce( + 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._( + contactInvitationManager: this, + signedContactInvitation: signedContactInvitation, + contactInvitation: contactInvitation, + contactRequestInboxKey: contactRequestInboxKey, + contactRequest: contactRequest, + contactRequestPrivate: contactRequestPrivate, + contactIdentityMaster: contactIdentityMaster, + writer: writer); + }); + + return out; + } + + Future checkInvitationStatus( + {required ActiveAccountInfo activeAccountInfo, + required proto.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 recordKey = proto.TypedKeyProto.fromProto( + contactInvitationRecord.contactRequestInbox.recordKey); + final writer = TypedKeyPair( + kind: recordKey.kind, key: writerKey, secret: writerSecret); + final acceptReject = await (await pool.openRead(recordKey, + crypto: await DHTRecordCryptoPrivate.fromTypedKeyPair(writer), + parent: accountRecordKey, + defaultSubkey: 1)) + .scope((contactRequestInbox) async { + // + final signedContactResponse = await contactRequestInbox.getProtobuf( + proto.SignedContactResponse.fromBuffer, + forceRefresh: true); + if (signedContactResponse == null) { + return null; + } + + final contactResponseBytes = + Uint8List.fromList(signedContactResponse.contactResponse); + final contactResponse = + proto.ContactResponse.fromBuffer(contactResponseBytes); + final contactIdentityMasterRecordKey = proto.TypedKeyProto.fromProto( + contactResponse.identityMasterRecordKey); + final cs = await pool.veilid.getCryptoSystem(recordKey.kind); + + // Fetch the remote contact's account master + final contactIdentityMaster = await openIdentityMaster( + identityMasterRecordKey: contactIdentityMasterRecordKey); + + // Verify + final signature = proto.SignatureProto.fromProto( + signedContactResponse.identitySignature); + await cs.verify(contactIdentityMaster.identityPublicKey, + contactResponseBytes, signature); + + // Check for rejection + if (!contactResponse.accept) { + return const InvitationStatus(acceptedContact: null); + } + + // Pull profile from remote conversation key + final remoteConversationRecordKey = proto.TypedKeyProto.fromProto( + contactResponse.remoteConversationRecordKey); + final remoteConversation = await readRemoteConversation( + activeAccountInfo: activeAccountInfo, + remoteIdentityPublicKey: + contactIdentityMaster.identityPublicTypedKey(), + remoteConversationRecordKey: remoteConversationRecordKey); + if (remoteConversation == null) { + log.info('Remote conversation could not be read. Waiting...'); + return null; + } + // Complete the local conversation now that we have the remote profile + final localConversationRecordKey = proto.TypedKeyProto.fromProto( + contactInvitationRecord.localConversationRecordKey); + return createConversation( + activeAccountInfo: activeAccountInfo, + remoteIdentityPublicKey: + contactIdentityMaster.identityPublicTypedKey(), + existingConversationRecordKey: localConversationRecordKey, + // ignore: prefer_expression_function_bodies + callback: (localConversation) async { + return InvitationStatus( + acceptedContact: AcceptedContact( + profile: remoteConversation.profile, + remoteIdentity: contactIdentityMaster, + remoteConversationRecordKey: remoteConversationRecordKey, + localConversationRecordKey: localConversationRecordKey)); + }); + }); + + if (acceptReject == null) { + return null; + } + + // Delete invitation and return the accepted or rejected contact + await deleteInvitation( + accepted: acceptReject.acceptedContact != null, + contactInvitationRecord: contactInvitationRecord); + + return acceptReject; + } on Exception catch (e) { + log.error('Exception in checkAcceptRejectContact: $e', e); + + // Attempt to clean up. All this needs better lifetime management + await deleteInvitation( + accepted: false, contactInvitationRecord: contactInvitationRecord); + + rethrow; + } + } + + // + + final ActiveAccountInfo _activeAccountInfo; + final DHTShortArray _dhtRecord; + IList _records; +} + +////////////////////////////////////////////////// +/// + +class ValidContactInvitation { + ValidContactInvitation._( + {required ContactInvitationListManager contactInvitationManager, + required proto.SignedContactInvitation signedContactInvitation, + required proto.ContactInvitation contactInvitation, + required TypedKey contactRequestInboxKey, + required proto.ContactRequest contactRequest, + required proto.ContactRequestPrivate contactRequestPrivate, + required IdentityMaster contactIdentityMaster, + required KeyPair writer}) + : _contactInvitationManager = contactInvitationManager, + _signedContactInvitation = signedContactInvitation, + _contactInvitation = contactInvitation, + _contactRequestInboxKey = contactRequestInboxKey, + _contactRequest = contactRequest, + _contactRequestPrivate = contactRequestPrivate, + _contactIdentityMaster = contactIdentityMaster, + _writer = writer; + + Future accept() async { + final pool = await DHTRecordPool.instance(); + final activeAccountInfo = _contactInvitationManager._activeAccountInfo; + try { + // Ensure we don't delete this if we're trying to chat to self + final isSelf = _contactIdentityMaster.identityPublicKey == + activeAccountInfo.localAccount.identityMaster.identityPublicKey; + final accountRecordKey = + activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey; + + return (await pool.openWrite(_contactRequestInboxKey, _writer, + parent: accountRecordKey)) + // ignore: prefer_expression_function_bodies + .maybeDeleteScope(!isSelf, (contactRequestInbox) async { + // Create local conversation key for this + // contact and send via contact response + return createConversation( + activeAccountInfo: activeAccountInfo, + remoteIdentityPublicKey: + _contactIdentityMaster.identityPublicTypedKey(), + callback: (localConversation) async { + final contactResponse = proto.ContactResponse() + ..accept = true + ..remoteConversationRecordKey = localConversation.key.toProto() + ..identityMasterRecordKey = activeAccountInfo + .localAccount.identityMaster.masterRecordKey + .toProto(); + final contactResponseBytes = contactResponse.writeToBuffer(); + + final cs = await pool.veilid + .getCryptoSystem(_contactRequestInboxKey.kind); + + final identitySignature = await cs.sign( + activeAccountInfo + .localAccount.identityMaster.identityPublicKey, + activeAccountInfo.userLogin.identitySecret.value, + contactResponseBytes); + + final signedContactResponse = proto.SignedContactResponse() + ..contactResponse = contactResponseBytes + ..identitySignature = identitySignature.toProto(); + + // Write the acceptance to the inbox + if (await contactRequestInbox.tryWriteProtobuf( + proto.SignedContactResponse.fromBuffer, + signedContactResponse, + subkey: 1) != + null) { + throw Exception('failed to accept contact invitation'); + } + return AcceptedContact( + profile: _contactRequestPrivate.profile, + remoteIdentity: _contactIdentityMaster, + remoteConversationRecordKey: proto.TypedKeyProto.fromProto( + _contactRequestPrivate.chatRecordKey), + localConversationRecordKey: localConversation.key, + ); + }); + }); + } on Exception catch (e) { + log.debug('exception: $e', e); + return null; + } + } + + Future reject() async { + final pool = await DHTRecordPool.instance(); + final activeAccountInfo = _contactInvitationManager._activeAccountInfo; + + // Ensure we don't delete this if we're trying to chat to self + final isSelf = _contactIdentityMaster.identityPublicKey == + activeAccountInfo.localAccount.identityMaster.identityPublicKey; + final accountRecordKey = + activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey; + + return (await pool.openWrite(_contactRequestInboxKey, _writer, + parent: accountRecordKey)) + .maybeDeleteScope(!isSelf, (contactRequestInbox) async { + final cs = + await pool.veilid.getCryptoSystem(_contactRequestInboxKey.kind); + + final contactResponse = proto.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 = proto.SignedContactResponse() + ..contactResponse = contactResponseBytes + ..identitySignature = identitySignature.toProto(); + + // Write the rejection to the inbox + if (await contactRequestInbox.tryWriteProtobuf( + proto.SignedContactResponse.fromBuffer, signedContactResponse, + subkey: 1) != + null) { + log.error('failed to reject contact invitation'); + return false; + } + return true; + }); + } + + // + ContactInvitationListManager _contactInvitationManager; + proto.SignedContactInvitation _signedContactInvitation; + proto.ContactInvitation _contactInvitation; + TypedKey _contactRequestInboxKey; + proto.ContactRequest _contactRequest; + proto.ContactRequestPrivate _contactRequestPrivate; + IdentityMaster _contactIdentityMaster; + KeyPair _writer; +} diff --git a/lib/providers/contact_invitation_list_manager.g.dart b/lib/providers/contact_invitation_list_manager.g.dart new file mode 100644 index 0000000..ce2f160 --- /dev/null +++ b/lib/providers/contact_invitation_list_manager.g.dart @@ -0,0 +1,202 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'contact_invitation_list_manager.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +String _$contactInvitationListManagerHash() => + r'8dda8e5005f0c0c921e3e8b7ce06e54bb5682085'; + +/// Copied from Dart SDK +class _SystemHash { + _SystemHash._(); + + static int combine(int hash, int value) { + // ignore: parameter_assignments + hash = 0x1fffffff & (hash + value); + // ignore: parameter_assignments + hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10)); + return hash ^ (hash >> 6); + } + + static int finish(int hash) { + // ignore: parameter_assignments + hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3)); + // ignore: parameter_assignments + hash = hash ^ (hash >> 11); + return 0x1fffffff & (hash + ((0x00003fff & hash) << 15)); + } +} + +abstract class _$ContactInvitationListManager + extends BuildlessAutoDisposeAsyncNotifier< + IList> { + late final ActiveAccountInfo activeAccountInfo; + + FutureOr> build( + ActiveAccountInfo activeAccountInfo, + ); +} + +////////////////////////////////////////////////// +////////////////////////////////////////////////// +/// +/// Copied from [ContactInvitationListManager]. +@ProviderFor(ContactInvitationListManager) +const contactInvitationListManagerProvider = + ContactInvitationListManagerFamily(); + +////////////////////////////////////////////////// +////////////////////////////////////////////////// +/// +/// Copied from [ContactInvitationListManager]. +class ContactInvitationListManagerFamily + extends Family>> { + ////////////////////////////////////////////////// +////////////////////////////////////////////////// + /// + /// Copied from [ContactInvitationListManager]. + const ContactInvitationListManagerFamily(); + + ////////////////////////////////////////////////// +////////////////////////////////////////////////// + /// + /// Copied from [ContactInvitationListManager]. + ContactInvitationListManagerProvider call( + ActiveAccountInfo activeAccountInfo, + ) { + return ContactInvitationListManagerProvider( + activeAccountInfo, + ); + } + + @override + ContactInvitationListManagerProvider getProviderOverride( + covariant ContactInvitationListManagerProvider provider, + ) { + return call( + provider.activeAccountInfo, + ); + } + + static const Iterable? _dependencies = null; + + @override + Iterable? get dependencies => _dependencies; + + static const Iterable? _allTransitiveDependencies = null; + + @override + Iterable? get allTransitiveDependencies => + _allTransitiveDependencies; + + @override + String? get name => r'contactInvitationListManagerProvider'; +} + +////////////////////////////////////////////////// +////////////////////////////////////////////////// +/// +/// Copied from [ContactInvitationListManager]. +class ContactInvitationListManagerProvider + extends AutoDisposeAsyncNotifierProviderImpl> { + ////////////////////////////////////////////////// +////////////////////////////////////////////////// + /// + /// Copied from [ContactInvitationListManager]. + ContactInvitationListManagerProvider( + ActiveAccountInfo activeAccountInfo, + ) : this._internal( + () => ContactInvitationListManager() + ..activeAccountInfo = activeAccountInfo, + from: contactInvitationListManagerProvider, + name: r'contactInvitationListManagerProvider', + debugGetCreateSourceHash: + const bool.fromEnvironment('dart.vm.product') + ? null + : _$contactInvitationListManagerHash, + dependencies: ContactInvitationListManagerFamily._dependencies, + allTransitiveDependencies: + ContactInvitationListManagerFamily._allTransitiveDependencies, + activeAccountInfo: activeAccountInfo, + ); + + ContactInvitationListManagerProvider._internal( + super._createNotifier, { + required super.name, + required super.dependencies, + required super.allTransitiveDependencies, + required super.debugGetCreateSourceHash, + required super.from, + required this.activeAccountInfo, + }) : super.internal(); + + final ActiveAccountInfo activeAccountInfo; + + @override + FutureOr> runNotifierBuild( + covariant ContactInvitationListManager notifier, + ) { + return notifier.build( + activeAccountInfo, + ); + } + + @override + Override overrideWith(ContactInvitationListManager Function() create) { + return ProviderOverride( + origin: this, + override: ContactInvitationListManagerProvider._internal( + () => create()..activeAccountInfo = activeAccountInfo, + from: from, + name: null, + dependencies: null, + allTransitiveDependencies: null, + debugGetCreateSourceHash: null, + activeAccountInfo: activeAccountInfo, + ), + ); + } + + @override + AutoDisposeAsyncNotifierProviderElement> createElement() { + return _ContactInvitationListManagerProviderElement(this); + } + + @override + bool operator ==(Object other) { + return other is ContactInvitationListManagerProvider && + other.activeAccountInfo == activeAccountInfo; + } + + @override + int get hashCode { + var hash = _SystemHash.combine(0, runtimeType.hashCode); + hash = _SystemHash.combine(hash, activeAccountInfo.hashCode); + + return _SystemHash.finish(hash); + } +} + +mixin ContactInvitationListManagerRef on AutoDisposeAsyncNotifierProviderRef< + IList> { + /// The parameter `activeAccountInfo` of this provider. + ActiveAccountInfo get activeAccountInfo; +} + +class _ContactInvitationListManagerProviderElement + extends AutoDisposeAsyncNotifierProviderElement< + ContactInvitationListManager, IList> + with ContactInvitationListManagerRef { + _ContactInvitationListManagerProviderElement(super.provider); + + @override + ActiveAccountInfo get activeAccountInfo => + (origin as ContactInvitationListManagerProvider).activeAccountInfo; +} +// ignore_for_file: type=lint +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member diff --git a/lib/providers/contact_invite.dart b/lib/providers/contact_invite.dart index e776409..bdbb8f7 100644 --- a/lib/providers/contact_invite.dart +++ b/lib/providers/contact_invite.dart @@ -13,509 +13,6 @@ import 'conversation.dart'; part 'contact_invite.g.dart'; -class ContactInviteInvalidKeyException implements Exception { - const ContactInviteInvalidKeyException(this.type) : super(); - final EncryptionKeyType type; -} - -class AcceptedContact { - AcceptedContact({ - required this.profile, - required this.remoteIdentity, - required this.remoteConversationRecordKey, - required this.localConversationRecordKey, - }); - - proto.Profile profile; - IdentityMaster remoteIdentity; - TypedKey remoteConversationRecordKey; - TypedKey localConversationRecordKey; -} - -class AcceptedOrRejectedContact { - AcceptedOrRejectedContact({required this.acceptedContact}); - AcceptedContact? acceptedContact; -} - -Future checkAcceptRejectContact( - {required ActiveAccountInfo activeAccountInfo, - required proto.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 recordKey = proto.TypedKeyProto.fromProto( - contactInvitationRecord.contactRequestInbox.recordKey); - final writer = TypedKeyPair( - kind: recordKey.kind, key: writerKey, secret: writerSecret); - final acceptReject = await (await pool.openRead(recordKey, - crypto: await DHTRecordCryptoPrivate.fromTypedKeyPair(writer), - parent: accountRecordKey, - defaultSubkey: 1)) - .scope((contactRequestInbox) async { - // - final signedContactResponse = await contactRequestInbox.getProtobuf( - proto.SignedContactResponse.fromBuffer, - forceRefresh: true); - if (signedContactResponse == null) { - return null; - } - - final contactResponseBytes = - Uint8List.fromList(signedContactResponse.contactResponse); - final contactResponse = - proto.ContactResponse.fromBuffer(contactResponseBytes); - final contactIdentityMasterRecordKey = proto.TypedKeyProto.fromProto( - contactResponse.identityMasterRecordKey); - final cs = await pool.veilid.getCryptoSystem(recordKey.kind); - - // Fetch the remote contact's account master - final contactIdentityMaster = await openIdentityMaster( - identityMasterRecordKey: contactIdentityMasterRecordKey); - - // Verify - final signature = proto.SignatureProto.fromProto( - signedContactResponse.identitySignature); - await cs.verify(contactIdentityMaster.identityPublicKey, - contactResponseBytes, signature); - - // Check for rejection - if (!contactResponse.accept) { - return AcceptedOrRejectedContact(acceptedContact: null); - } - - // Pull profile from remote conversation key - final remoteConversationRecordKey = proto.TypedKeyProto.fromProto( - contactResponse.remoteConversationRecordKey); - final remoteConversation = await readRemoteConversation( - activeAccountInfo: activeAccountInfo, - remoteIdentityPublicKey: - contactIdentityMaster.identityPublicTypedKey(), - remoteConversationRecordKey: remoteConversationRecordKey); - if (remoteConversation == null) { - log.info('Remote conversation could not be read. Waiting...'); - return null; - } - // Complete the local conversation now that we have the remote profile - final localConversationRecordKey = proto.TypedKeyProto.fromProto( - contactInvitationRecord.localConversationRecordKey); - return createConversation( - activeAccountInfo: activeAccountInfo, - remoteIdentityPublicKey: - contactIdentityMaster.identityPublicTypedKey(), - existingConversationRecordKey: localConversationRecordKey, - // ignore: prefer_expression_function_bodies - callback: (localConversation) async { - return AcceptedOrRejectedContact( - acceptedContact: AcceptedContact( - profile: remoteConversation.profile, - remoteIdentity: contactIdentityMaster, - remoteConversationRecordKey: remoteConversationRecordKey, - localConversationRecordKey: localConversationRecordKey)); - }); - }); - - if (acceptReject == null) { - return null; - } - - // Delete invitation and return the accepted or rejected contact - await deleteContactInvitation( - accepted: acceptReject.acceptedContact != null, - activeAccountInfo: activeAccountInfo, - contactInvitationRecord: contactInvitationRecord); - - return acceptReject; - } on Exception catch (e) { - log.error('Exception in checkAcceptRejectContact: $e', e); - - // Attempt to clean up. All this needs better lifetime management - await deleteContactInvitation( - accepted: false, - activeAccountInfo: activeAccountInfo, - contactInvitationRecord: contactInvitationRecord); - - rethrow; - } -} - -Future deleteContactInvitation( - {required bool accepted, - required ActiveAccountInfo activeAccountInfo, - required proto.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 Exception('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.openRead( - proto.TypedKeyProto.fromProto( - contactInvitationRecord.localConversationRecordKey), - 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 contactRequestWriter = await cs.generateKeyPair(); - final conversationWriter = - getConversationWriter(activeAccountInfo: activeAccountInfo); - - // Encrypt the writer secret with the encryption key - final encryptedSecret = await encryptSecretToBytes( - secret: contactRequestWriter.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, - schema: DHTSchema.smpl(oCnt: 0, members: [ - DHTSchemaMember(mKey: conversationWriter.key, mCnt: 1) - ]))) - .deleteScope((localConversation) async { - // dont bother reopening localConversation with writer - // Make ContactRequestPrivate and encrypt with the writer secret - final crpriv = proto.ContactRequestPrivate() - ..writerKey = contactRequestWriter.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.encryptAeadWithNonce(crprivbytes, contactRequestWriter.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: contactRequestWriter.key) - ]), - crypto: const DHTRecordCryptoPublic())) - .deleteScope((contactRequestInbox) async { - // Store ContactRequest in owner subkey - await contactRequestInbox.eventualWriteProtobuf(creq); - - // Create ContactInvitation and SignedContactInvitation - final cinv = proto.ContactInvitation() - ..contactRequestInboxKey = contactRequestInbox.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() - ..contactRequestInbox = - contactRequestInbox.ownedDHTRecordPointer.toProto() - ..writerKey = contactRequestWriter.key.toProto() - ..writerSecret = contactRequestWriter.secret.toProto() - ..localConversationRecordKey = localConversation.key.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 Exception('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}); - - proto.SignedContactInvitation signedContactInvitation; - proto.ContactInvitation contactInvitation; - TypedKey contactRequestInboxKey; - proto.ContactRequest contactRequest; - proto.ContactRequestPrivate contactRequestPrivate; - IdentityMaster contactIdentityMaster; - KeyPair writer; -} - -typedef GetEncryptionKeyCallback = Future Function( - VeilidCryptoSystem cs, - EncryptionKeyType encryptionKeyType, - Uint8List encryptedSecret); - -Future validateContactInvitation( - {required ActiveAccountInfo activeAccountInfo, - required IList? contactInvitationRecords, - required Uint8List inviteData, - required GetEncryptionKeyCallback getEncryptionKeyCallback}) async { - final accountRecordKey = - activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey; - - 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); - - ValidContactInvitation? out; - - final pool = await DHTRecordPool.instance(); - final cs = await pool.veilid.getCryptoSystem(contactRequestInboxKey.kind); - - // See if we're chatting to ourselves, if so, don't delete it here - final isSelf = contactInvitationRecords?.indexWhere((cir) => - proto.TypedKeyProto.fromProto(cir.contactRequestInbox.recordKey) == - contactRequestInboxKey) != - -1; - - await (await pool.openRead(contactRequestInboxKey, parent: accountRecordKey)) - .maybeDeleteScope(!isSelf, (contactRequestInbox) async { - // - final contactRequest = - await contactRequestInbox.getProtobuf(proto.ContactRequest.fromBuffer); - - // Decrypt contact request private - final encryptionKeyType = - EncryptionKeyType.fromProto(contactRequest!.encryptionKeyType); - late final SharedSecret? writerSecret; - try { - writerSecret = await getEncryptionKeyCallback(cs, encryptionKeyType, - Uint8List.fromList(contactInvitation.writerSecret)); - } on Exception catch (_) { - throw ContactInviteInvalidKeyException(encryptionKeyType); - } - if (writerSecret == null) { - return null; - } - - final contactRequestPrivateBytes = await cs.decryptAeadWithNonce( - 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; -} - -Future acceptContactInvitation( - ActiveAccountInfo activeAccountInfo, - ValidContactInvitation validContactInvitation) async { - final pool = await DHTRecordPool.instance(); - try { - // Ensure we don't delete this if we're trying to chat to self - final isSelf = - validContactInvitation.contactIdentityMaster.identityPublicKey == - activeAccountInfo.localAccount.identityMaster.identityPublicKey; - final accountRecordKey = - activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey; - - return (await pool.openWrite(validContactInvitation.contactRequestInboxKey, - validContactInvitation.writer, - parent: accountRecordKey)) - // ignore: prefer_expression_function_bodies - .maybeDeleteScope(!isSelf, (contactRequestInbox) async { - // Create local conversation key for this - // contact and send via contact response - return createConversation( - activeAccountInfo: activeAccountInfo, - remoteIdentityPublicKey: validContactInvitation.contactIdentityMaster - .identityPublicTypedKey(), - callback: (localConversation) async { - final contactResponse = proto.ContactResponse() - ..accept = true - ..remoteConversationRecordKey = localConversation.key.toProto() - ..identityMasterRecordKey = activeAccountInfo - .localAccount.identityMaster.masterRecordKey - .toProto(); - final contactResponseBytes = contactResponse.writeToBuffer(); - - final cs = await pool.veilid.getCryptoSystem( - validContactInvitation.contactRequestInboxKey.kind); - - final identitySignature = await cs.sign( - activeAccountInfo.localAccount.identityMaster.identityPublicKey, - activeAccountInfo.userLogin.identitySecret.value, - contactResponseBytes); - - final signedContactResponse = proto.SignedContactResponse() - ..contactResponse = contactResponseBytes - ..identitySignature = identitySignature.toProto(); - - // Write the acceptance to the inbox - if (await contactRequestInbox.tryWriteProtobuf( - proto.SignedContactResponse.fromBuffer, - signedContactResponse, - subkey: 1) != - null) { - throw Exception('failed to accept contact invitation'); - } - return AcceptedContact( - profile: validContactInvitation.contactRequestPrivate.profile, - remoteIdentity: validContactInvitation.contactIdentityMaster, - remoteConversationRecordKey: proto.TypedKeyProto.fromProto( - validContactInvitation.contactRequestPrivate.chatRecordKey), - localConversationRecordKey: localConversation.key, - ); - }); - }); - } on Exception catch (e) { - log.debug('exception: $e', e); - return null; - } -} - -Future rejectContactInvitation(ActiveAccountInfo activeAccountInfo, - ValidContactInvitation validContactInvitation) async { - final pool = await DHTRecordPool.instance(); - - // Ensure we don't delete this if we're trying to chat to self - final isSelf = - validContactInvitation.contactIdentityMaster.identityPublicKey == - activeAccountInfo.localAccount.identityMaster.identityPublicKey; - final accountRecordKey = - activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey; - - return (await pool.openWrite(validContactInvitation.contactRequestInboxKey, - validContactInvitation.writer, - parent: accountRecordKey)) - .maybeDeleteScope(!isSelf, (contactRequestInbox) async { - final cs = await pool.veilid - .getCryptoSystem(validContactInvitation.contactRequestInboxKey.kind); - - final contactResponse = proto.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 = proto.SignedContactResponse() - ..contactResponse = contactResponseBytes - ..identitySignature = identitySignature.toProto(); - - // Write the rejection to the inbox - if (await contactRequestInbox.tryWriteProtobuf( - proto.SignedContactResponse.fromBuffer, signedContactResponse, - subkey: 1) != - null) { - log.error('failed to reject contact invitation'); - return false; - } - return true; - }); -} - /// Get the active account contact invitation list @riverpod Future?> fetchContactInvitationRecords( diff --git a/lib/providers/contact_invite.g.dart b/lib/providers/contact_invite.g.dart index b6cf257..758a54a 100644 --- a/lib/providers/contact_invite.g.dart +++ b/lib/providers/contact_invite.g.dart @@ -7,14 +7,14 @@ part of 'contact_invite.dart'; // ************************************************************************** String _$fetchContactInvitationRecordsHash() => - r'365d563c5e66f45679f597502ea9e4b8296ff8af'; + r'ff0b2c68d42cb106602982b1fb56a7bd8183c04a'; /// Get the active account contact invitation list /// /// Copied from [fetchContactInvitationRecords]. @ProviderFor(fetchContactInvitationRecords) final fetchContactInvitationRecordsProvider = - AutoDisposeFutureProvider?>.internal( + AutoDisposeFutureProvider?>.internal( fetchContactInvitationRecords, name: r'fetchContactInvitationRecordsProvider', debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') @@ -25,6 +25,6 @@ final fetchContactInvitationRecordsProvider = ); typedef FetchContactInvitationRecordsRef - = AutoDisposeFutureProviderRef?>; + = AutoDisposeFutureProviderRef?>; // ignore_for_file: type=lint // ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member diff --git a/lib/providers/conversation.dart b/lib/providers/conversation.dart index 617ead3..bac8cda 100644 --- a/lib/providers/conversation.dart +++ b/lib/providers/conversation.dart @@ -1,3 +1,7 @@ +// A Conversation is a type of Chat that is 1:1 between two Contacts only +// Each Contact in the ContactList has at most one Conversation between the +// remote contact and the local account + import 'dart:convert'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; @@ -14,25 +18,230 @@ import 'contact.dart'; part 'conversation.g.dart'; -Future getConversationCrypto({ - required ActiveAccountInfo activeAccountInfo, - required TypedKey remoteIdentityPublicKey, -}) async { - final veilid = await eventualVeilid.future; - final identitySecret = activeAccountInfo.userLogin.identitySecret; - final cs = await veilid.getCryptoSystem(identitySecret.kind); - final sharedSecret = - await cs.cachedDH(remoteIdentityPublicKey.value, identitySecret.value); - return DHTRecordCryptoPrivate.fromSecret(identitySecret.kind, sharedSecret); -} +class Conversation { + Conversation._( + {required ActiveAccountInfo activeAccountInfo, + required TypedKey localConversationRecordKey, + required TypedKey remoteIdentityPublicKey, + required TypedKey remoteConversationRecordKey}) + : _activeAccountInfo = activeAccountInfo, + _localConversationRecordKey = localConversationRecordKey, + _remoteIdentityPublicKey = remoteIdentityPublicKey, + _remoteConversationRecordKey = remoteConversationRecordKey; -KeyPair getConversationWriter({ - required ActiveAccountInfo activeAccountInfo, -}) { - final identityKey = - activeAccountInfo.localAccount.identityMaster.identityPublicKey; - final identitySecret = activeAccountInfo.userLogin.identitySecret; - return KeyPair(key: identityKey, secret: identitySecret.value); + Future open() async {} + + Future close() async { + // + } + + Future readRemoteConversation() async { + final accountRecordKey = + _activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey; + final pool = await DHTRecordPool.instance(); + + final crypto = await getConversationCrypto(); + return (await pool.openRead(_remoteConversationRecordKey, + parent: accountRecordKey, crypto: crypto)) + .scope((remoteConversation) async { + // + final conversation = + await remoteConversation.getProtobuf(proto.Conversation.fromBuffer); + return conversation; + }); + } + + Future readLocalConversation() async { + final accountRecordKey = + _activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey; + final pool = await DHTRecordPool.instance(); + + final crypto = await getConversationCrypto(); + return (await pool.openRead(_localConversationRecordKey, + parent: accountRecordKey, crypto: crypto)) + .scope((localConversation) async { + // + final update = + await localConversation.getProtobuf(proto.Conversation.fromBuffer); + if (update != null) { + return update; + } + return null; + }); + } + + Future writeLocalConversation({ + required proto.Conversation conversation, + }) async { + final accountRecordKey = + _activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey; + final pool = await DHTRecordPool.instance(); + + final crypto = await getConversationCrypto(); + final writer = _activeAccountInfo.getConversationWriter(); + + return (await pool.openWrite(_localConversationRecordKey, writer, + parent: accountRecordKey, crypto: crypto)) + .scope((localConversation) async { + // + final update = await localConversation.tryWriteProtobuf( + proto.Conversation.fromBuffer, conversation); + if (update != null) { + return update; + } + return null; + }); + } + + Future addLocalConversationMessage( + {required proto.Message message}) async { + final conversation = await readLocalConversation(); + if (conversation == null) { + return; + } + final messagesRecordKey = + proto.TypedKeyProto.fromProto(conversation.messages); + final crypto = await getConversationCrypto(); + final writer = _activeAccountInfo.getConversationWriter(); + + await (await DHTShortArray.openWrite(messagesRecordKey, writer, + parent: _localConversationRecordKey, crypto: crypto)) + .scope((messages) async { + await messages.tryAddItem(message.writeToBuffer()); + }); + } + + Future mergeLocalConversationMessages( + {required IList newMessages}) async { + final conversation = await readLocalConversation(); + if (conversation == null) { + return false; + } + var changed = false; + final messagesRecordKey = + proto.TypedKeyProto.fromProto(conversation.messages); + final crypto = await getConversationCrypto(); + final writer = _activeAccountInfo.getConversationWriter(); + + newMessages = newMessages.sort((a, b) => Timestamp.fromInt64(a.timestamp) + .compareTo(Timestamp.fromInt64(b.timestamp))); + + await (await DHTShortArray.openWrite(messagesRecordKey, writer, + parent: _localConversationRecordKey, crypto: crypto)) + .scope((messages) async { + // Ensure newMessages is sorted by timestamp + newMessages = + newMessages.sort((a, b) => a.timestamp.compareTo(b.timestamp)); + + // Existing messages will always be sorted by timestamp so merging is easy + var pos = 0; + outer: + for (final newMessage in newMessages) { + var skip = false; + while (pos < messages.length) { + final m = + await messages.getItemProtobuf(proto.Message.fromBuffer, pos); + if (m == null) { + log.error('unable to get message #$pos'); + break outer; + } + + // If timestamp to insert is less than + // the current position, insert it here + final newTs = Timestamp.fromInt64(newMessage.timestamp); + final curTs = Timestamp.fromInt64(m.timestamp); + final cmp = newTs.compareTo(curTs); + if (cmp < 0) { + break; + } else if (cmp == 0) { + skip = true; + break; + } + pos++; + } + // Insert at this position + if (!skip) { + await messages.tryInsertItem(pos, newMessage.writeToBuffer()); + changed = true; + } + } + }); + return changed; + } + + Future?> getRemoteConversationMessages() async { + final conversation = await readRemoteConversation(); + if (conversation == null) { + return null; + } + final messagesRecordKey = + proto.TypedKeyProto.fromProto(conversation.messages); + final crypto = await getConversationCrypto(); + + return (await DHTShortArray.openRead(messagesRecordKey, + parent: _remoteConversationRecordKey, crypto: crypto)) + .scope((messages) async { + var out = IList(); + for (var i = 0; i < messages.length; i++) { + final msg = await messages.getItemProtobuf(proto.Message.fromBuffer, i); + if (msg == null) { + throw Exception('Failed to get message'); + } + out = out.add(msg); + } + return out; + }); + } + + // + + Future getConversationCrypto() async { + var conversationCrypto = _conversationCrypto; + if (conversationCrypto != null) { + return conversationCrypto; + } + final veilid = await eventualVeilid.future; + final identitySecret = _activeAccountInfo.userLogin.identitySecret; + final cs = await veilid.getCryptoSystem(identitySecret.kind); + final sharedSecret = + await cs.cachedDH(_remoteIdentityPublicKey.value, identitySecret.value); + + conversationCrypto = await DHTRecordCryptoPrivate.fromSecret( + identitySecret.kind, sharedSecret); + _conversationCrypto = conversationCrypto; + return conversationCrypto; + } + + Future?> getLocalConversationMessages() async { + final conversation = await readLocalConversation(); + if (conversation == null) { + return null; + } + final messagesRecordKey = + proto.TypedKeyProto.fromProto(conversation.messages); + final crypto = await getConversationCrypto(); + + return (await DHTShortArray.openRead(messagesRecordKey, + parent: _localConversationRecordKey, crypto: crypto)) + .scope((messages) async { + var out = IList(); + for (var i = 0; i < messages.length; i++) { + final msg = await messages.getItemProtobuf(proto.Message.fromBuffer, i); + if (msg == null) { + throw Exception('Failed to get message'); + } + out = out.add(msg); + } + return out; + }); + } + + final ActiveAccountInfo _activeAccountInfo; + final TypedKey _localConversationRecordKey; + final TypedKey _remoteIdentityPublicKey; + final TypedKey _remoteConversationRecordKey; + // + DHTRecordCrypto? _conversationCrypto; } // Create a conversation @@ -51,7 +260,7 @@ Future createConversation( final crypto = await getConversationCrypto( activeAccountInfo: activeAccountInfo, remoteIdentityPublicKey: remoteIdentityPublicKey); - final writer = getConversationWriter(activeAccountInfo: activeAccountInfo); + final writer = activeAccountInfo.getConversationWriter(); // Open with SMPL scheme for identity writer late final DHTRecord localConversationRecord; @@ -95,238 +304,10 @@ Future createConversation( }); } -Future readRemoteConversation({ - required ActiveAccountInfo activeAccountInfo, - required TypedKey remoteConversationRecordKey, - required TypedKey remoteIdentityPublicKey, -}) async { - final accountRecordKey = - activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey; - final pool = await DHTRecordPool.instance(); - - final crypto = await getConversationCrypto( - activeAccountInfo: activeAccountInfo, - remoteIdentityPublicKey: remoteIdentityPublicKey); - return (await pool.openRead(remoteConversationRecordKey, - parent: accountRecordKey, crypto: crypto)) - .scope((remoteConversation) async { - // - final conversation = - await remoteConversation.getProtobuf(proto.Conversation.fromBuffer); - return conversation; - }); -} - -Future writeLocalConversation({ - required ActiveAccountInfo activeAccountInfo, - required TypedKey localConversationRecordKey, - required TypedKey remoteIdentityPublicKey, - required proto.Conversation conversation, -}) async { - final accountRecordKey = - activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey; - final pool = await DHTRecordPool.instance(); - - final crypto = await getConversationCrypto( - activeAccountInfo: activeAccountInfo, - remoteIdentityPublicKey: remoteIdentityPublicKey); - final writer = getConversationWriter(activeAccountInfo: activeAccountInfo); - - return (await pool.openWrite(localConversationRecordKey, writer, - parent: accountRecordKey, crypto: crypto)) - .scope((localConversation) async { - // - final update = await localConversation.tryWriteProtobuf( - proto.Conversation.fromBuffer, conversation); - if (update != null) { - return update; - } - return null; - }); -} - -Future readLocalConversation({ - required ActiveAccountInfo activeAccountInfo, - required TypedKey localConversationRecordKey, - required TypedKey remoteIdentityPublicKey, -}) async { - final accountRecordKey = - activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey; - final pool = await DHTRecordPool.instance(); - - final crypto = await getConversationCrypto( - activeAccountInfo: activeAccountInfo, - remoteIdentityPublicKey: remoteIdentityPublicKey); - - return (await pool.openRead(localConversationRecordKey, - parent: accountRecordKey, crypto: crypto)) - .scope((localConversation) async { - // - final update = - await localConversation.getProtobuf(proto.Conversation.fromBuffer); - if (update != null) { - return update; - } - return null; - }); -} - -Future addLocalConversationMessage( - {required ActiveAccountInfo activeAccountInfo, - required TypedKey localConversationRecordKey, - required TypedKey remoteIdentityPublicKey, - required proto.Message message}) async { - final conversation = await readLocalConversation( - activeAccountInfo: activeAccountInfo, - localConversationRecordKey: localConversationRecordKey, - remoteIdentityPublicKey: remoteIdentityPublicKey); - if (conversation == null) { - return; - } - final messagesRecordKey = - proto.TypedKeyProto.fromProto(conversation.messages); - final crypto = await getConversationCrypto( - activeAccountInfo: activeAccountInfo, - remoteIdentityPublicKey: remoteIdentityPublicKey); - final writer = getConversationWriter(activeAccountInfo: activeAccountInfo); - - await (await DHTShortArray.openWrite(messagesRecordKey, writer, - parent: localConversationRecordKey, crypto: crypto)) - .scope((messages) async { - await messages.tryAddItem(message.writeToBuffer()); - }); -} - -Future mergeLocalConversationMessages( - {required ActiveAccountInfo activeAccountInfo, - required TypedKey localConversationRecordKey, - required TypedKey remoteIdentityPublicKey, - required IList newMessages}) async { - final conversation = await readLocalConversation( - activeAccountInfo: activeAccountInfo, - localConversationRecordKey: localConversationRecordKey, - remoteIdentityPublicKey: remoteIdentityPublicKey); - if (conversation == null) { - return false; - } - var changed = false; - final messagesRecordKey = - proto.TypedKeyProto.fromProto(conversation.messages); - final crypto = await getConversationCrypto( - activeAccountInfo: activeAccountInfo, - remoteIdentityPublicKey: remoteIdentityPublicKey); - final writer = getConversationWriter(activeAccountInfo: activeAccountInfo); - - newMessages = newMessages.sort((a, b) => Timestamp.fromInt64(a.timestamp) - .compareTo(Timestamp.fromInt64(b.timestamp))); - - await (await DHTShortArray.openWrite(messagesRecordKey, writer, - parent: localConversationRecordKey, crypto: crypto)) - .scope((messages) async { - // Ensure newMessages is sorted by timestamp - newMessages = - newMessages.sort((a, b) => a.timestamp.compareTo(b.timestamp)); - - // Existing messages will always be sorted by timestamp so merging is easy - var pos = 0; - outer: - for (final newMessage in newMessages) { - var skip = false; - while (pos < messages.length) { - final m = await messages.getItemProtobuf(proto.Message.fromBuffer, pos); - if (m == null) { - log.error('unable to get message #$pos'); - break outer; - } - - // If timestamp to insert is less than - // the current position, insert it here - final newTs = Timestamp.fromInt64(newMessage.timestamp); - final curTs = Timestamp.fromInt64(m.timestamp); - final cmp = newTs.compareTo(curTs); - if (cmp < 0) { - break; - } else if (cmp == 0) { - skip = true; - break; - } - pos++; - } - // Insert at this position - if (!skip) { - await messages.tryInsertItem(pos, newMessage.writeToBuffer()); - changed = true; - } - } - }); - return changed; -} - -Future?> getLocalConversationMessages({ - required ActiveAccountInfo activeAccountInfo, - required TypedKey localConversationRecordKey, - required TypedKey remoteIdentityPublicKey, -}) async { - final conversation = await readLocalConversation( - activeAccountInfo: activeAccountInfo, - localConversationRecordKey: localConversationRecordKey, - remoteIdentityPublicKey: remoteIdentityPublicKey); - if (conversation == null) { - return null; - } - final messagesRecordKey = - proto.TypedKeyProto.fromProto(conversation.messages); - final crypto = await getConversationCrypto( - activeAccountInfo: activeAccountInfo, - remoteIdentityPublicKey: remoteIdentityPublicKey); - - return (await DHTShortArray.openRead(messagesRecordKey, - parent: localConversationRecordKey, crypto: crypto)) - .scope((messages) async { - var out = IList(); - for (var i = 0; i < messages.length; i++) { - final msg = await messages.getItemProtobuf(proto.Message.fromBuffer, i); - if (msg == null) { - throw Exception('Failed to get message'); - } - out = out.add(msg); - } - return out; - }); -} - -Future?> getRemoteConversationMessages({ - required ActiveAccountInfo activeAccountInfo, - required TypedKey remoteConversationRecordKey, - required TypedKey remoteIdentityPublicKey, -}) async { - final conversation = await readRemoteConversation( - activeAccountInfo: activeAccountInfo, - remoteConversationRecordKey: remoteConversationRecordKey, - remoteIdentityPublicKey: remoteIdentityPublicKey); - if (conversation == null) { - return null; - } - final messagesRecordKey = - proto.TypedKeyProto.fromProto(conversation.messages); - final crypto = await getConversationCrypto( - activeAccountInfo: activeAccountInfo, - remoteIdentityPublicKey: remoteIdentityPublicKey); - - return (await DHTShortArray.openRead(messagesRecordKey, - parent: remoteConversationRecordKey, crypto: crypto)) - .scope((messages) async { - var out = IList(); - for (var i = 0; i < messages.length; i++) { - final msg = await messages.getItemProtobuf(proto.Message.fromBuffer, i); - if (msg == null) { - throw Exception('Failed to get message'); - } - out = out.add(msg); - } - return out; - }); -} +// +// +// +// @riverpod class ActiveConversationMessages extends _$ActiveConversationMessages { diff --git a/lib/providers/conversation.g.dart b/lib/providers/conversation.g.dart index fcf007c..a4875dd 100644 --- a/lib/providers/conversation.g.dart +++ b/lib/providers/conversation.g.dart @@ -7,12 +7,12 @@ part of 'conversation.dart'; // ************************************************************************** String _$activeConversationMessagesHash() => - r'61c9e16f1304c7929a971ec7711d2b6c7cadc5ea'; + r'5579a9386f2046b156720ae799a0e77aca119b09'; /// See also [ActiveConversationMessages]. @ProviderFor(ActiveConversationMessages) final activeConversationMessagesProvider = AutoDisposeAsyncNotifierProvider< - ActiveConversationMessages, IList?>.internal( + ActiveConversationMessages, IList?>.internal( ActiveConversationMessages.new, name: r'activeConversationMessagesProvider', debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') @@ -23,6 +23,6 @@ final activeConversationMessagesProvider = AutoDisposeAsyncNotifierProvider< ); typedef _$ActiveConversationMessages - = AutoDisposeAsyncNotifier?>; + = AutoDisposeAsyncNotifier?>; // ignore_for_file: type=lint // ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member diff --git a/lib/providers/local_accounts.dart b/lib/providers/local_accounts.dart index c7c793d..4294adf 100644 --- a/lib/providers/local_accounts.dart +++ b/lib/providers/local_accounts.dart @@ -14,7 +14,7 @@ part 'local_accounts.g.dart'; const String veilidChatAccountKey = 'com.veilid.veilidchat'; -// Local account manager +// Local accounts table @riverpod class LocalAccounts extends _$LocalAccounts with AsyncTableDBBacked> { From 29210c89d27da75a1d28cf86c732f1eb2a29c013 Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Tue, 26 Dec 2023 20:26:54 -0500 Subject: [PATCH 03/68] break everything --- lib/app.dart | 47 +- lib/entities/local_account.dart | 76 --- lib/init.dart | 47 ++ .../account_repository.dart | 285 +++++++++++ .../account_repository/active_logins.dart | 25 + .../active_logins.freezed.dart | 181 +++++++ .../account_repository/active_logins.g.dart | 24 + .../encryption_key_type.dart | 40 ++ .../account_repository/local_account.dart | 39 ++ .../local_account.freezed.dart | 0 .../account_repository}/local_account.g.dart | 0 .../account_repository}/user_login.dart | 21 +- .../user_login.freezed.dart | 166 ------ .../account_repository}/user_login.g.dart | 17 - .../active_user_login_cubit.dart | 42 ++ .../active_user_login_state.dart | 3 + .../local_account_manager.dart | 3 + .../local_accounts_cubit.dart | 42 ++ .../local_accounts_state.dart | 3 + .../user_logins_cubit/user_logins_cubit.dart | 42 ++ .../user_logins_cubit/user_logins_state.dart | 3 + lib/main.dart | 16 +- .../components/account_bubble.dart | 0 .../bottom_sheet_action_button.dart | 0 .../components/chat_component.dart | 0 .../chat_single_contact_item_widget.dart | 2 +- .../chat_single_contact_list_widget.dart | 0 .../contact_invitation_display.dart | 0 .../contact_invitation_item_widget.dart | 0 .../contact_invitation_list_widget.dart | 0 .../components/contact_item_widget.dart | 2 +- .../components/contact_list_widget.dart | 0 .../components/default_app_bar.dart | 0 .../components/empty_chat_list_widget.dart | 0 .../components/empty_chat_widget.dart | 0 .../components/empty_contact_list_widget.dart | 0 .../components/enter_password.dart | 0 .../components/enter_pin.dart | 0 .../components/invite_dialog.dart | 0 .../components/no_conversation_widget.dart | 0 .../components/paste_invite_dialog.dart | 0 .../components/profile_widget.dart | 0 .../components/scan_invite_dialog.dart | 0 .../components/send_invite_dialog.dart | 0 .../components/signal_strength_meter.dart | 0 .../entities/entities.dart | 0 .../entities/preferences.dart | 47 -- .../entities/preferences.freezed.dart | 213 +------- .../entities/preferences.g.dart | 0 .../managers/contact_list_manager.dart | 0 .../managers/valid_contact_invitation.dart | 0 .../pages/chat_only.dart | 2 +- .../pages/developer.dart | 6 +- .../pages/edit_account.dart | 0 .../pages/edit_contact.dart | 0 lib/{ => old_to_refactor}/pages/home.dart | 18 +- lib/{ => old_to_refactor}/pages/index.dart | 9 +- .../pages/main_pager/account.dart | 14 +- .../pages/main_pager/chats.dart | 14 +- .../pages/main_pager/main_pager.dart | 16 +- .../pages/new_account.dart | 14 +- lib/{ => old_to_refactor}/pages/settings.dart | 10 +- .../providers/account.dart | 10 +- lib/{ => old_to_refactor}/providers/chat.dart | 4 +- .../providers/connection_state.dart | 2 +- .../providers/connection_state.freezed.dart | 0 .../providers/contact.dart | 6 +- .../contact_invitation_list_manager.dart | 8 +- .../providers/contact_invite.dart | 8 +- .../providers/conversation.dart | 8 +- .../providers/window_control.dart | 2 +- lib/processor.dart | 2 +- lib/providers/account.g.dart | 200 -------- lib/providers/chat.g.dart | 28 - lib/providers/contact.g.dart | 29 -- .../contact_invitation_list_manager.g.dart | 202 -------- lib/providers/contact_invite.g.dart | 30 -- lib/providers/conversation.g.dart | 28 - lib/providers/local_accounts.dart | 167 ------ lib/providers/local_accounts.g.dart | 179 ------- lib/providers/logins.dart | 150 ------ lib/providers/logins.g.dart | 176 ------- lib/providers/window_control.g.dart | 26 - lib/router/cubit/router_cubit.dart | 152 ++++++ lib/router/cubit/router_cubit.freezed.dart | 193 +++++++ lib/router/cubit/router_cubit.g.dart | 21 + lib/router/cubit/router_state.dart | 12 + lib/router/make_router.dart | 20 + lib/router/router.dart | 25 +- lib/router/router.g.dart | 26 - lib/router/router_notifier.dart | 160 ------ lib/router/router_notifier.g.dart | 26 - lib/{tools => theme}/radix_generator.dart | 3 +- lib/theme/scale_color.dart | 91 ++++ lib/theme/scale_scheme.dart | 53 ++ lib/theme/theme.dart | 3 + lib/theme/theme_preference.dart | 52 ++ lib/theme/theme_preference.freezed.dart | 201 ++++++++ lib/theme/theme_preference.g.dart | 24 + lib/theme/theme_repository.dart | 132 +++++ lib/theme/theme_service.dart | 108 ++++ lib/tick.dart | 17 +- lib/tools/async_table_db_backed_cubit.dart | 51 ++ lib/tools/async_value.dart | 172 +++++++ lib/tools/async_value.freezed.dart | 480 ++++++++++++++++++ lib/tools/loggy.dart | 7 +- lib/tools/secret_crypto.dart | 2 +- lib/tools/stack_trace.dart | 12 + lib/tools/state_logger.dart | 43 +- lib/tools/stream_listenable.dart | 34 ++ lib/tools/theme_service.dart | 255 ---------- lib/tools/theme_service.g.dart | 24 - lib/tools/tools.dart | 4 +- lib/tools/widget_helpers.dart | 2 +- lib/veilid_init.dart | 78 --- lib/veilid_init.g.dart | 25 - .../dht_support/src/dht_record_pool.dart | 2 +- lib/veilid_support/src/config.dart | 33 +- lib/veilid_support/src/table_db.dart | 49 +- pubspec.lock | 144 ++---- pubspec.yaml | 10 +- 121 files changed, 2892 insertions(+), 2608 deletions(-) delete mode 100644 lib/entities/local_account.dart create mode 100644 lib/init.dart create mode 100644 lib/local_account_manager/account_repository/account_repository.dart create mode 100644 lib/local_account_manager/account_repository/active_logins.dart create mode 100644 lib/local_account_manager/account_repository/active_logins.freezed.dart create mode 100644 lib/local_account_manager/account_repository/active_logins.g.dart create mode 100644 lib/local_account_manager/account_repository/encryption_key_type.dart create mode 100644 lib/local_account_manager/account_repository/local_account.dart rename lib/{entities => local_account_manager/account_repository}/local_account.freezed.dart (100%) rename lib/{entities => local_account_manager/account_repository}/local_account.g.dart (100%) rename lib/{entities => local_account_manager/account_repository}/user_login.dart (56%) rename lib/{entities => local_account_manager/account_repository}/user_login.freezed.dart (63%) rename lib/{entities => local_account_manager/account_repository}/user_login.g.dart (62%) create mode 100644 lib/local_account_manager/active_user_login_cubit/active_user_login_cubit.dart create mode 100644 lib/local_account_manager/active_user_login_cubit/active_user_login_state.dart create mode 100644 lib/local_account_manager/local_account_manager.dart create mode 100644 lib/local_account_manager/local_accounts_cubit/local_accounts_cubit.dart create mode 100644 lib/local_account_manager/local_accounts_cubit/local_accounts_state.dart create mode 100644 lib/local_account_manager/user_logins_cubit/user_logins_cubit.dart create mode 100644 lib/local_account_manager/user_logins_cubit/user_logins_state.dart rename lib/{ => old_to_refactor}/components/account_bubble.dart (100%) rename lib/{ => old_to_refactor}/components/bottom_sheet_action_button.dart (100%) rename lib/{ => old_to_refactor}/components/chat_component.dart (100%) rename lib/{ => old_to_refactor}/components/chat_single_contact_item_widget.dart (99%) rename lib/{ => old_to_refactor}/components/chat_single_contact_list_widget.dart (100%) rename lib/{ => old_to_refactor}/components/contact_invitation_display.dart (100%) rename lib/{ => old_to_refactor}/components/contact_invitation_item_widget.dart (100%) rename lib/{ => old_to_refactor}/components/contact_invitation_list_widget.dart (100%) rename lib/{ => old_to_refactor}/components/contact_item_widget.dart (99%) rename lib/{ => old_to_refactor}/components/contact_list_widget.dart (100%) rename lib/{ => old_to_refactor}/components/default_app_bar.dart (100%) rename lib/{ => old_to_refactor}/components/empty_chat_list_widget.dart (100%) rename lib/{ => old_to_refactor}/components/empty_chat_widget.dart (100%) rename lib/{ => old_to_refactor}/components/empty_contact_list_widget.dart (100%) rename lib/{ => old_to_refactor}/components/enter_password.dart (100%) rename lib/{ => old_to_refactor}/components/enter_pin.dart (100%) rename lib/{ => old_to_refactor}/components/invite_dialog.dart (100%) rename lib/{ => old_to_refactor}/components/no_conversation_widget.dart (100%) rename lib/{ => old_to_refactor}/components/paste_invite_dialog.dart (100%) rename lib/{ => old_to_refactor}/components/profile_widget.dart (100%) rename lib/{ => old_to_refactor}/components/scan_invite_dialog.dart (100%) rename lib/{ => old_to_refactor}/components/send_invite_dialog.dart (100%) rename lib/{ => old_to_refactor}/components/signal_strength_meter.dart (100%) rename lib/{ => old_to_refactor}/entities/entities.dart (100%) rename lib/{ => old_to_refactor}/entities/preferences.dart (55%) rename lib/{ => old_to_refactor}/entities/preferences.freezed.dart (64%) rename lib/{ => old_to_refactor}/entities/preferences.g.dart (100%) rename lib/{ => old_to_refactor}/managers/contact_list_manager.dart (100%) rename lib/{ => old_to_refactor}/managers/valid_contact_invitation.dart (100%) rename lib/{ => old_to_refactor}/pages/chat_only.dart (95%) rename lib/{ => old_to_refactor}/pages/developer.dart (98%) rename lib/{ => old_to_refactor}/pages/edit_account.dart (100%) rename lib/{ => old_to_refactor}/pages/edit_contact.dart (100%) rename lib/{ => old_to_refactor}/pages/home.dart (95%) rename lib/{ => old_to_refactor}/pages/index.dart (87%) rename lib/{ => old_to_refactor}/pages/main_pager/account.dart (89%) rename lib/{ => old_to_refactor}/pages/main_pager/chats.dart (88%) rename lib/{ => old_to_refactor}/pages/main_pager/main_pager.dart (96%) rename lib/{ => old_to_refactor}/pages/new_account.dart (94%) rename lib/{ => old_to_refactor}/pages/settings.dart (95%) rename lib/{ => old_to_refactor}/providers/account.dart (94%) rename lib/{ => old_to_refactor}/providers/chat.dart (97%) rename lib/{ => old_to_refactor}/providers/connection_state.dart (95%) rename lib/{ => old_to_refactor}/providers/connection_state.freezed.dart (100%) rename lib/{ => old_to_refactor}/providers/contact.dart (97%) rename lib/{ => old_to_refactor}/providers/contact_invitation_list_manager.dart (99%) rename lib/{ => old_to_refactor}/providers/contact_invite.dart (90%) rename lib/{ => old_to_refactor}/providers/conversation.dart (98%) rename lib/{ => old_to_refactor}/providers/window_control.dart (98%) delete mode 100644 lib/providers/account.g.dart delete mode 100644 lib/providers/chat.g.dart delete mode 100644 lib/providers/contact.g.dart delete mode 100644 lib/providers/contact_invitation_list_manager.g.dart delete mode 100644 lib/providers/contact_invite.g.dart delete mode 100644 lib/providers/conversation.g.dart delete mode 100644 lib/providers/local_accounts.dart delete mode 100644 lib/providers/local_accounts.g.dart delete mode 100644 lib/providers/logins.dart delete mode 100644 lib/providers/logins.g.dart delete mode 100644 lib/providers/window_control.g.dart create mode 100644 lib/router/cubit/router_cubit.dart create mode 100644 lib/router/cubit/router_cubit.freezed.dart create mode 100644 lib/router/cubit/router_cubit.g.dart create mode 100644 lib/router/cubit/router_state.dart create mode 100644 lib/router/make_router.dart delete mode 100644 lib/router/router.g.dart delete mode 100644 lib/router/router_notifier.dart delete mode 100644 lib/router/router_notifier.g.dart rename lib/{tools => theme}/radix_generator.dart (99%) create mode 100644 lib/theme/scale_color.dart create mode 100644 lib/theme/scale_scheme.dart create mode 100644 lib/theme/theme.dart create mode 100644 lib/theme/theme_preference.dart create mode 100644 lib/theme/theme_preference.freezed.dart create mode 100644 lib/theme/theme_preference.g.dart create mode 100644 lib/theme/theme_repository.dart create mode 100644 lib/theme/theme_service.dart create mode 100644 lib/tools/async_table_db_backed_cubit.dart create mode 100644 lib/tools/async_value.dart create mode 100644 lib/tools/async_value.freezed.dart create mode 100644 lib/tools/stack_trace.dart create mode 100644 lib/tools/stream_listenable.dart delete mode 100644 lib/tools/theme_service.dart delete mode 100644 lib/tools/theme_service.g.dart delete mode 100644 lib/veilid_init.dart delete mode 100644 lib/veilid_init.g.dart diff --git a/lib/app.dart b/lib/app.dart index 957f46f..2510f69 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -1,45 +1,50 @@ import 'package:animated_theme_switcher/animated_theme_switcher.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_translate/flutter_translate.dart'; import 'package:form_builder_validators/form_builder_validators.dart'; import 'router/router.dart'; import 'tick.dart'; -class VeilidChatApp extends ConsumerWidget { +class VeilidChatApp extends StatelessWidget { const VeilidChatApp({ - required this.theme, + required this.themeData, super.key, }); - final ThemeData theme; + final ThemeData themeData; @override - Widget build(BuildContext context, WidgetRef ref) { - final router = ref.watch(routerProvider); + Widget build(BuildContext context) { final localizationDelegate = LocalizedApp.of(context).delegate; return ThemeProvider( - initTheme: theme, + initTheme: themeData, builder: (_, theme) => LocalizationProvider( state: LocalizationProvider.of(context).state, child: BackgroundTicker( - builder: (context) => MaterialApp.router( - debugShowCheckedModeBanner: false, - routerConfig: router, - title: translate('app.title'), - theme: theme, - localizationsDelegates: [ - GlobalMaterialLocalizations.delegate, - GlobalWidgetsLocalizations.delegate, - FormBuilderLocalizations.delegate, - localizationDelegate - ], - supportedLocales: localizationDelegate.supportedLocales, - locale: localizationDelegate.currentLocale, + builder: (context) => BlocProvider( + create: (context) => RouterCubit(), + child: MaterialApp.router( + debugShowCheckedModeBanner: false, + routerConfig: router( + routerCubit: + BlocProvider.of(context)), + title: translate('app.title'), + theme: theme, + localizationsDelegates: [ + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + FormBuilderLocalizations.delegate, + localizationDelegate + ], + supportedLocales: + localizationDelegate.supportedLocales, + locale: localizationDelegate.currentLocale, + ), )), )); } @@ -47,6 +52,6 @@ class VeilidChatApp extends ConsumerWidget { @override void debugFillProperties(DiagnosticPropertiesBuilder properties) { super.debugFillProperties(properties); - properties.add(DiagnosticsProperty('theme', theme)); + properties.add(DiagnosticsProperty('themeData', themeData)); } } diff --git a/lib/entities/local_account.dart b/lib/entities/local_account.dart deleted file mode 100644 index 68c5ca2..0000000 --- a/lib/entities/local_account.dart +++ /dev/null @@ -1,76 +0,0 @@ -import 'dart:typed_data'; - -import 'package:change_case/change_case.dart'; -import 'package:freezed_annotation/freezed_annotation.dart'; - -import '../proto/proto.dart' as proto; -import '../veilid_support/veilid_support.dart'; - -part 'local_account.freezed.dart'; -part 'local_account.g.dart'; - -// Local account identitySecretKey is potentially encrypted with a key -// using the following mechanisms -// * None : no key, bytes are unencrypted -// * Pin : Code is a numeric pin (4-256 numeric digits) hashed with Argon2 -// * Password: Code is a UTF-8 string that is hashed with Argon2 -enum EncryptionKeyType { - none, - pin, - password; - - 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 -// and represents the accounts that have been added/imported -// on the current device. -// Stores a copy of the IdentityMaster associated with the account -// and the identitySecretKey optionally encrypted by an unlock code -// This is the root of the account information tree for VeilidChat -// -@freezed -class LocalAccount with _$LocalAccount { - const factory LocalAccount({ - // The master key record for the account, containing the identityPublicKey - required IdentityMaster identityMaster, - // The encrypted identity secret that goes with - // the identityPublicKey with appended salt - @Uint8ListJsonConverter() required Uint8List identitySecretBytes, - // The kind of encryption input used on the account - required EncryptionKeyType encryptionKeyType, - // If account is not hidden, password can be retrieved via - required bool biometricsEnabled, - // Keep account hidden unless account password is entered - // (tries all hidden accounts with auth method (no biometrics)) - required bool hiddenAccount, - // Display name for account until it is unlocked - required String name, - }) = _LocalAccount; - - factory LocalAccount.fromJson(dynamic json) => - _$LocalAccountFromJson(json as Map); -} diff --git a/lib/init.dart b/lib/init.dart new file mode 100644 index 0000000..3305e33 --- /dev/null +++ b/lib/init.dart @@ -0,0 +1,47 @@ +import 'dart:async'; + +import 'local_account_manager/local_account_manager.dart'; +import 'processor.dart'; +import 'tools/tools.dart'; +import 'veilid_support/veilid_support.dart'; + +const String appName = 'VeilidChat'; + +final Completer eventualVeilid = Completer(); +final Processor processor = Processor(); + +final Completer eventualInitialized = Completer(); + +// Initialize Veilid +Future initializeVeilid() async { + // Ensure this runs only once + if (eventualVeilid.isCompleted) { + return; + } + + // Init Veilid + Veilid.instance.initializeVeilidCore(getDefaultVeilidPlatformConfig(appName)); + + // Veilid logging + initVeilidLog(); + + // Startup Veilid + await processor.startup(); + + // Share the initialized veilid instance to the rest of the app + eventualVeilid.complete(Veilid.instance); +} + +// Initialize repositories +Future initializeRepositories() async { + await AccountRepository.instance; +} + +Future initializeVeilidChat() async { + log.info("Initializing Veilid"); + await initializeVeilid(); + log.info("Initializing Repositories"); + await initializeRepositories(); + + eventualInitialized.complete(); +} diff --git a/lib/local_account_manager/account_repository/account_repository.dart b/lib/local_account_manager/account_repository/account_repository.dart new file mode 100644 index 0000000..04be9e5 --- /dev/null +++ b/lib/local_account_manager/account_repository/account_repository.dart @@ -0,0 +1,285 @@ +import 'package:fast_immutable_collections/fast_immutable_collections.dart'; + +import '../../../proto/proto.dart' as proto; +import '../../tools/tools.dart'; +import '../../veilid_support/veilid_support.dart'; +import 'active_logins.dart'; +import 'encryption_key_type.dart'; +import 'local_account.dart'; +import 'user_login.dart'; + +export 'active_logins.dart'; +export 'encryption_key_type.dart'; +export 'local_account.dart'; +export 'user_login.dart'; + +const String veilidChatAccountKey = 'com.veilid.veilidchat'; + +enum AccountRepositoryChange { localAccounts, userLogins, activeUserLogin } + +class AccountRepository { + AccountRepository._() + : _localAccounts = _initLocalAccounts(), + _activeLogins = _initActiveLogins(); + + static TableDBValue> _initLocalAccounts() => TableDBValue( + tableName: 'local_account_manager', + tableKeyName: 'local_accounts', + valueFromJson: (obj) => obj != null + ? IList.fromJson( + obj, genericFromJson(LocalAccount.fromJson)) + : IList(), + valueToJson: (val) => val.toJson((la) => la.toJson())); + + static TableDBValue _initActiveLogins() => TableDBValue( + tableName: 'local_account_manager', + tableKeyName: 'active_logins', + valueFromJson: (obj) => obj != null + ? ActiveLogins.fromJson(obj as Map) + : ActiveLogins.empty(), + valueToJson: (val) => val.toJson()); + + final TableDBValue> _localAccounts; + final TableDBValue _activeLogins; + + ////////////////////////////////////////////////////////////// + /// Singleton initialization + + static AccountRepository? _instance; + static Future get instance async { + if (_instance == null) { + final accountRepository = AccountRepository._(); + await accountRepository.init(); + _instance = accountRepository; + } + return _instance!; + } + + Future init() async { + await _localAccounts.load(); + await _activeLogins.load(); + } + + ////////////////////////////////////////////////////////////// + /// Streams + + Stream changes() async* {} + + ////////////////////////////////////////////////////////////// + /// Selectors + IList getLocalAccounts() => _localAccounts.requireValue; + IList getUserLogins() => _activeLogins.requireValue.userLogins; + TypedKey? getActiveUserLogin() => _activeLogins.requireValue.activeUserLogin; + + LocalAccount? fetchLocalAccount({required TypedKey accountMasterRecordKey}) { + final localAccounts = _localAccounts.requireValue; + final idx = localAccounts.indexWhere( + (e) => e.identityMaster.masterRecordKey == accountMasterRecordKey); + if (idx == -1) { + return null; + } + return localAccounts[idx]; + } + + UserLogin? fetchLogin({required TypedKey accountMasterRecordKey}) { + final userLogins = _activeLogins.requireValue.userLogins; + final idx = userLogins + .indexWhere((e) => e.accountMasterRecordKey == accountMasterRecordKey); + if (idx == -1) { + return null; + } + return userLogins[idx]; + } + + ////////////////////////////////////////////////////////////// + /// Mutators + + /// Reorder accounts + Future reorderAccount(int oldIndex, int newIndex) async { + final localAccounts = await _localAccounts.get(); + final removedItem = Output(); + final updated = localAccounts + .removeAt(oldIndex, removedItem) + .insert(newIndex, removedItem.value!); + await _localAccounts.set(updated); + } + + /// Creates a new Account associated with master identity + /// Adds a logged-out LocalAccount to track its existence on this device + Future newLocalAccount( + {required IdentityMaster identityMaster, + required SecretKey identitySecret, + required String name, + required String pronouns, + EncryptionKeyType encryptionKeyType = EncryptionKeyType.none, + String encryptionKey = ''}) async { + final localAccounts = await _localAccounts.get(); + + // Add account with profile to DHT + await identityMaster.addAccountToIdentity( + identitySecret: identitySecret, + accountKey: veilidChatAccountKey, + createAccountCallback: (parent) async { + // Make empty contact list + final contactList = await (await DHTShortArray.create(parent: parent)) + .scope((r) async => r.record.ownedDHTRecordPointer); + + // Make empty contact invitation record list + final contactInvitationRecords = + await (await DHTShortArray.create(parent: parent)) + .scope((r) async => r.record.ownedDHTRecordPointer); + + // Make empty chat record list + final chatRecords = await (await DHTShortArray.create(parent: parent)) + .scope((r) async => r.record.ownedDHTRecordPointer); + + // Make account object + final account = proto.Account() + ..profile = (proto.Profile() + ..name = name + ..pronouns = pronouns) + ..contactList = contactList.toProto() + ..contactInvitationRecords = contactInvitationRecords.toProto() + ..chatList = chatRecords.toProto(); + return account; + }); + + // Encrypt identitySecret with key + final identitySecretBytes = await encryptSecretToBytes( + secret: identitySecret, + cryptoKind: identityMaster.identityRecordKey.kind, + encryptionKey: encryptionKey, + encryptionKeyType: encryptionKeyType); + + // Create local account object + // Does not contain the account key or its secret + // as that is not to be persisted, and only pulled from the identity key + // and optionally decrypted with the unlock password + final localAccount = LocalAccount( + identityMaster: identityMaster, + identitySecretBytes: identitySecretBytes, + encryptionKeyType: encryptionKeyType, + biometricsEnabled: false, + hiddenAccount: false, + name: name, + ); + + // Add local account object to internal store + final newLocalAccounts = localAccounts.add(localAccount); + + await _localAccounts.set(newLocalAccounts); + + // Return local account object + return localAccount; + } + + /// Remove an account and wipe the messages for this account from this device + Future deleteLocalAccount(TypedKey accountMasterRecordKey) async { + await logout(accountMasterRecordKey); + + final localAccounts = await _localAccounts.get(); + final newLocalAccounts = localAccounts.removeWhere( + (la) => la.identityMaster.masterRecordKey == accountMasterRecordKey); + + await _localAccounts.set(newLocalAccounts); + + // TO DO: wipe messages + + return true; + } + + /// Import an account from another VeilidChat instance + + /// Recover an account with the master identity secret + + /// Delete an account from all devices + + Future switchToAccount(TypedKey? accountMasterRecordKey) async { + final activeLogins = await _activeLogins.get(); + + if (accountMasterRecordKey != null) { + // Assert the specified record key can be found, will throw if not + final _ = activeLogins.userLogins.firstWhere( + (ul) => ul.accountMasterRecordKey == accountMasterRecordKey); + } + final newActiveLogins = + activeLogins.copyWith(activeUserLogin: accountMasterRecordKey); + await _activeLogins.set(newActiveLogins); + } + + Future _decryptedLogin( + IdentityMaster identityMaster, SecretKey identitySecret) async { + final cs = await Veilid.instance + .getCryptoSystem(identityMaster.identityRecordKey.kind); + final keyOk = await cs.validateKeyPair( + identityMaster.identityPublicKey, identitySecret); + if (!keyOk) { + throw Exception('Identity is corrupted'); + } + + // Read the identity key to get the account keys + final accountRecordInfo = await identityMaster.readAccountFromIdentity( + identitySecret: identitySecret, accountKey: veilidChatAccountKey); + + // Add to user logins and select it + final activeLogins = await _activeLogins.get(); + final now = Veilid.instance.now(); + final newActiveLogins = activeLogins.copyWith( + userLogins: activeLogins.userLogins.replaceFirstWhere( + (ul) => ul.accountMasterRecordKey == identityMaster.masterRecordKey, + (ul) => ul != null + ? ul.copyWith(lastActive: now) + : UserLogin( + accountMasterRecordKey: identityMaster.masterRecordKey, + identitySecret: + TypedSecret(kind: cs.kind(), value: identitySecret), + accountRecordInfo: accountRecordInfo, + lastActive: now), + addIfNotFound: true), + activeUserLogin: identityMaster.masterRecordKey); + await _activeLogins.set(newActiveLogins); + + return true; + } + + Future login(TypedKey accountMasterRecordKey, + EncryptionKeyType encryptionKeyType, String encryptionKey) async { + final localAccounts = await _localAccounts.get(); + + // Get account, throws if not found + final localAccount = localAccounts.firstWhere( + (la) => la.identityMaster.masterRecordKey == accountMasterRecordKey); + + // Log in with this local account + + // Derive key from password + if (localAccount.encryptionKeyType != encryptionKeyType) { + throw Exception('Wrong authentication type'); + } + + final identitySecret = await decryptSecretFromBytes( + secretBytes: localAccount.identitySecretBytes, + cryptoKind: localAccount.identityMaster.identityRecordKey.kind, + encryptionKeyType: localAccount.encryptionKeyType, + encryptionKey: encryptionKey, + ); + + // Validate this secret with the identity public key and log in + return _decryptedLogin(localAccount.identityMaster, identitySecret); + } + + Future logout(TypedKey? accountMasterRecordKey) async { + final activeLogins = await _activeLogins.get(); + final logoutUser = accountMasterRecordKey ?? activeLogins.activeUserLogin; + if (logoutUser == null) { + return; + } + final newActiveLogins = activeLogins.copyWith( + activeUserLogin: activeLogins.activeUserLogin == logoutUser + ? null + : activeLogins.activeUserLogin, + userLogins: activeLogins.userLogins + .removeWhere((ul) => ul.accountMasterRecordKey == logoutUser)); + await _activeLogins.set(newActiveLogins); + } +} diff --git a/lib/local_account_manager/account_repository/active_logins.dart b/lib/local_account_manager/account_repository/active_logins.dart new file mode 100644 index 0000000..fd83e37 --- /dev/null +++ b/lib/local_account_manager/account_repository/active_logins.dart @@ -0,0 +1,25 @@ +// Represents a set of user logins and the currently selected account +import 'package:fast_immutable_collections/fast_immutable_collections.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; + +import '../../veilid_support/veilid_support.dart'; +import 'user_login.dart'; + +part 'active_logins.g.dart'; +part 'active_logins.freezed.dart'; + +@freezed +class ActiveLogins with _$ActiveLogins { + const factory ActiveLogins({ + // The list of current logged in accounts + required IList userLogins, + // The current selected account indexed by master record key + TypedKey? activeUserLogin, + }) = _ActiveLogins; + + factory ActiveLogins.empty() => + const ActiveLogins(userLogins: IListConst([])); + + factory ActiveLogins.fromJson(dynamic json) => + _ActiveLogins.fromJson(json as Map); +} diff --git a/lib/local_account_manager/account_repository/active_logins.freezed.dart b/lib/local_account_manager/account_repository/active_logins.freezed.dart new file mode 100644 index 0000000..d824640 --- /dev/null +++ b/lib/local_account_manager/account_repository/active_logins.freezed.dart @@ -0,0 +1,181 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'active_logins.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +final _privateConstructorUsedError = UnsupportedError( + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#custom-getters-and-methods'); + +ActiveLogins _$ActiveLoginsFromJson(Map json) { + return _ActiveLogins.fromJson(json); +} + +/// @nodoc +mixin _$ActiveLogins { +// The list of current logged in accounts + IList get userLogins => + throw _privateConstructorUsedError; // The current selected account indexed by master record key + Typed? get activeUserLogin => + throw _privateConstructorUsedError; + + Map toJson() => throw _privateConstructorUsedError; + @JsonKey(ignore: true) + $ActiveLoginsCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $ActiveLoginsCopyWith<$Res> { + factory $ActiveLoginsCopyWith( + ActiveLogins value, $Res Function(ActiveLogins) then) = + _$ActiveLoginsCopyWithImpl<$Res, ActiveLogins>; + @useResult + $Res call( + {IList userLogins, + Typed? activeUserLogin}); +} + +/// @nodoc +class _$ActiveLoginsCopyWithImpl<$Res, $Val extends ActiveLogins> + implements $ActiveLoginsCopyWith<$Res> { + _$ActiveLoginsCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? userLogins = null, + Object? activeUserLogin = freezed, + }) { + return _then(_value.copyWith( + userLogins: null == userLogins + ? _value.userLogins + : userLogins // ignore: cast_nullable_to_non_nullable + as IList, + activeUserLogin: freezed == activeUserLogin + ? _value.activeUserLogin + : activeUserLogin // ignore: cast_nullable_to_non_nullable + as Typed?, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$ActiveLoginsImplCopyWith<$Res> + implements $ActiveLoginsCopyWith<$Res> { + factory _$$ActiveLoginsImplCopyWith( + _$ActiveLoginsImpl value, $Res Function(_$ActiveLoginsImpl) then) = + __$$ActiveLoginsImplCopyWithImpl<$Res>; + @override + @useResult + $Res call( + {IList userLogins, + Typed? activeUserLogin}); +} + +/// @nodoc +class __$$ActiveLoginsImplCopyWithImpl<$Res> + extends _$ActiveLoginsCopyWithImpl<$Res, _$ActiveLoginsImpl> + implements _$$ActiveLoginsImplCopyWith<$Res> { + __$$ActiveLoginsImplCopyWithImpl( + _$ActiveLoginsImpl _value, $Res Function(_$ActiveLoginsImpl) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? userLogins = null, + Object? activeUserLogin = freezed, + }) { + return _then(_$ActiveLoginsImpl( + userLogins: null == userLogins + ? _value.userLogins + : userLogins // ignore: cast_nullable_to_non_nullable + as IList, + activeUserLogin: freezed == activeUserLogin + ? _value.activeUserLogin + : activeUserLogin // ignore: cast_nullable_to_non_nullable + as Typed?, + )); + } +} + +/// @nodoc +@JsonSerializable() +class _$ActiveLoginsImpl implements _ActiveLogins { + const _$ActiveLoginsImpl({required this.userLogins, this.activeUserLogin}); + + factory _$ActiveLoginsImpl.fromJson(Map json) => + _$$ActiveLoginsImplFromJson(json); + +// The list of current logged in accounts + @override + final IList userLogins; +// The current selected account indexed by master record key + @override + final Typed? activeUserLogin; + + @override + String toString() { + return 'ActiveLogins(userLogins: $userLogins, activeUserLogin: $activeUserLogin)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$ActiveLoginsImpl && + const DeepCollectionEquality() + .equals(other.userLogins, userLogins) && + (identical(other.activeUserLogin, activeUserLogin) || + other.activeUserLogin == activeUserLogin)); + } + + @JsonKey(ignore: true) + @override + int get hashCode => Object.hash(runtimeType, + const DeepCollectionEquality().hash(userLogins), activeUserLogin); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$ActiveLoginsImplCopyWith<_$ActiveLoginsImpl> get copyWith => + __$$ActiveLoginsImplCopyWithImpl<_$ActiveLoginsImpl>(this, _$identity); + + @override + Map toJson() { + return _$$ActiveLoginsImplToJson( + this, + ); + } +} + +abstract class _ActiveLogins implements ActiveLogins { + const factory _ActiveLogins( + {required final IList userLogins, + final Typed? activeUserLogin}) = _$ActiveLoginsImpl; + + factory _ActiveLogins.fromJson(Map json) = + _$ActiveLoginsImpl.fromJson; + + @override // The list of current logged in accounts + IList get userLogins; + @override // The current selected account indexed by master record key + Typed? get activeUserLogin; + @override + @JsonKey(ignore: true) + _$$ActiveLoginsImplCopyWith<_$ActiveLoginsImpl> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/lib/local_account_manager/account_repository/active_logins.g.dart b/lib/local_account_manager/account_repository/active_logins.g.dart new file mode 100644 index 0000000..95646a6 --- /dev/null +++ b/lib/local_account_manager/account_repository/active_logins.g.dart @@ -0,0 +1,24 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'active_logins.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +_$ActiveLoginsImpl _$$ActiveLoginsImplFromJson(Map json) => + _$ActiveLoginsImpl( + userLogins: IList.fromJson( + json['user_logins'], (value) => UserLogin.fromJson(value)), + activeUserLogin: json['active_user_login'] == null + ? null + : Typed.fromJson(json['active_user_login']), + ); + +Map _$$ActiveLoginsImplToJson(_$ActiveLoginsImpl instance) => + { + 'user_logins': instance.userLogins.toJson( + (value) => value.toJson(), + ), + 'active_user_login': instance.activeUserLogin?.toJson(), + }; diff --git a/lib/local_account_manager/account_repository/encryption_key_type.dart b/lib/local_account_manager/account_repository/encryption_key_type.dart new file mode 100644 index 0000000..45fd682 --- /dev/null +++ b/lib/local_account_manager/account_repository/encryption_key_type.dart @@ -0,0 +1,40 @@ +// Local account identitySecretKey is potentially encrypted with a key +// using the following mechanisms +// * None : no key, bytes are unencrypted +// * Pin : Code is a numeric pin (4-256 numeric digits) hashed with Argon2 +// * Password: Code is a UTF-8 string that is hashed with Argon2 + +import 'package:change_case/change_case.dart'; + +import '../../../proto/proto.dart' as proto; + +enum EncryptionKeyType { + none, + pin, + password; + + 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, + }; +} diff --git a/lib/local_account_manager/account_repository/local_account.dart b/lib/local_account_manager/account_repository/local_account.dart new file mode 100644 index 0000000..6aa2c4a --- /dev/null +++ b/lib/local_account_manager/account_repository/local_account.dart @@ -0,0 +1,39 @@ +import 'dart:typed_data'; + +import 'package:freezed_annotation/freezed_annotation.dart'; + +import '../../veilid_support/veilid_support.dart'; +import 'encryption_key_type.dart'; + +part 'local_account.g.dart'; +part 'local_account.freezed.dart'; + +// Local Accounts are stored in a table locally and not backed by a DHT key +// and represents the accounts that have been added/imported +// on the current device. +// Stores a copy of the IdentityMaster associated with the account +// and the identitySecretKey optionally encrypted by an unlock code +// This is the root of the account information tree for VeilidChat +// +@freezed +class LocalAccount with _$LocalAccount { + const factory LocalAccount({ + // The master key record for the account, containing the identityPublicKey + required IdentityMaster identityMaster, + // The encrypted identity secret that goes with + // the identityPublicKey with appended salt + @Uint8ListJsonConverter() required Uint8List identitySecretBytes, + // The kind of encryption input used on the account + required EncryptionKeyType encryptionKeyType, + // If account is not hidden, password can be retrieved via + required bool biometricsEnabled, + // Keep account hidden unless account password is entered + // (tries all hidden accounts with auth method (no biometrics)) + required bool hiddenAccount, + // Display name for account until it is unlocked + required String name, + }) = _LocalAccount; + + factory LocalAccount.fromJson(dynamic json) => + _$LocalAccountFromJson(json as Map); +} diff --git a/lib/entities/local_account.freezed.dart b/lib/local_account_manager/account_repository/local_account.freezed.dart similarity index 100% rename from lib/entities/local_account.freezed.dart rename to lib/local_account_manager/account_repository/local_account.freezed.dart diff --git a/lib/entities/local_account.g.dart b/lib/local_account_manager/account_repository/local_account.g.dart similarity index 100% rename from lib/entities/local_account.g.dart rename to lib/local_account_manager/account_repository/local_account.g.dart diff --git a/lib/entities/user_login.dart b/lib/local_account_manager/account_repository/user_login.dart similarity index 56% rename from lib/entities/user_login.dart rename to lib/local_account_manager/account_repository/user_login.dart index 55a4fb2..2708ce4 100644 --- a/lib/entities/user_login.dart +++ b/lib/local_account_manager/account_repository/user_login.dart @@ -1,7 +1,6 @@ -import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; -import '../veilid_support/veilid_support.dart'; +import '../../veilid_support/veilid_support.dart'; part 'user_login.freezed.dart'; part 'user_login.g.dart'; @@ -26,21 +25,3 @@ class UserLogin with _$UserLogin { factory UserLogin.fromJson(dynamic json) => _$UserLoginFromJson(json as Map); } - -// Represents a set of user logins -// and the currently selected account -@freezed -class ActiveLogins with _$ActiveLogins { - const factory ActiveLogins({ - // The list of current logged in accounts - required IList userLogins, - // The current selected account indexed by master record key - TypedKey? activeUserLogin, - }) = _ActiveLogins; - - factory ActiveLogins.empty() => - const ActiveLogins(userLogins: IListConst([])); - - factory ActiveLogins.fromJson(dynamic json) => - _$ActiveLoginsFromJson(json as Map); -} diff --git a/lib/entities/user_login.freezed.dart b/lib/local_account_manager/account_repository/user_login.freezed.dart similarity index 63% rename from lib/entities/user_login.freezed.dart rename to lib/local_account_manager/account_repository/user_login.freezed.dart index 9aa3b68..89eb637 100644 --- a/lib/entities/user_login.freezed.dart +++ b/lib/local_account_manager/account_repository/user_login.freezed.dart @@ -238,169 +238,3 @@ abstract class _UserLogin implements UserLogin { _$$UserLoginImplCopyWith<_$UserLoginImpl> get copyWith => throw _privateConstructorUsedError; } - -ActiveLogins _$ActiveLoginsFromJson(Map json) { - return _ActiveLogins.fromJson(json); -} - -/// @nodoc -mixin _$ActiveLogins { -// The list of current logged in accounts - IList get userLogins => - throw _privateConstructorUsedError; // The current selected account indexed by master record key - Typed? get activeUserLogin => - throw _privateConstructorUsedError; - - Map toJson() => throw _privateConstructorUsedError; - @JsonKey(ignore: true) - $ActiveLoginsCopyWith get copyWith => - throw _privateConstructorUsedError; -} - -/// @nodoc -abstract class $ActiveLoginsCopyWith<$Res> { - factory $ActiveLoginsCopyWith( - ActiveLogins value, $Res Function(ActiveLogins) then) = - _$ActiveLoginsCopyWithImpl<$Res, ActiveLogins>; - @useResult - $Res call( - {IList userLogins, - Typed? activeUserLogin}); -} - -/// @nodoc -class _$ActiveLoginsCopyWithImpl<$Res, $Val extends ActiveLogins> - implements $ActiveLoginsCopyWith<$Res> { - _$ActiveLoginsCopyWithImpl(this._value, this._then); - - // ignore: unused_field - final $Val _value; - // ignore: unused_field - final $Res Function($Val) _then; - - @pragma('vm:prefer-inline') - @override - $Res call({ - Object? userLogins = null, - Object? activeUserLogin = freezed, - }) { - return _then(_value.copyWith( - userLogins: null == userLogins - ? _value.userLogins - : userLogins // ignore: cast_nullable_to_non_nullable - as IList, - activeUserLogin: freezed == activeUserLogin - ? _value.activeUserLogin - : activeUserLogin // ignore: cast_nullable_to_non_nullable - as Typed?, - ) as $Val); - } -} - -/// @nodoc -abstract class _$$ActiveLoginsImplCopyWith<$Res> - implements $ActiveLoginsCopyWith<$Res> { - factory _$$ActiveLoginsImplCopyWith( - _$ActiveLoginsImpl value, $Res Function(_$ActiveLoginsImpl) then) = - __$$ActiveLoginsImplCopyWithImpl<$Res>; - @override - @useResult - $Res call( - {IList userLogins, - Typed? activeUserLogin}); -} - -/// @nodoc -class __$$ActiveLoginsImplCopyWithImpl<$Res> - extends _$ActiveLoginsCopyWithImpl<$Res, _$ActiveLoginsImpl> - implements _$$ActiveLoginsImplCopyWith<$Res> { - __$$ActiveLoginsImplCopyWithImpl( - _$ActiveLoginsImpl _value, $Res Function(_$ActiveLoginsImpl) _then) - : super(_value, _then); - - @pragma('vm:prefer-inline') - @override - $Res call({ - Object? userLogins = null, - Object? activeUserLogin = freezed, - }) { - return _then(_$ActiveLoginsImpl( - userLogins: null == userLogins - ? _value.userLogins - : userLogins // ignore: cast_nullable_to_non_nullable - as IList, - activeUserLogin: freezed == activeUserLogin - ? _value.activeUserLogin - : activeUserLogin // ignore: cast_nullable_to_non_nullable - as Typed?, - )); - } -} - -/// @nodoc -@JsonSerializable() -class _$ActiveLoginsImpl implements _ActiveLogins { - const _$ActiveLoginsImpl({required this.userLogins, this.activeUserLogin}); - - factory _$ActiveLoginsImpl.fromJson(Map json) => - _$$ActiveLoginsImplFromJson(json); - -// The list of current logged in accounts - @override - final IList userLogins; -// The current selected account indexed by master record key - @override - final Typed? activeUserLogin; - - @override - String toString() { - return 'ActiveLogins(userLogins: $userLogins, activeUserLogin: $activeUserLogin)'; - } - - @override - bool operator ==(Object other) { - return identical(this, other) || - (other.runtimeType == runtimeType && - other is _$ActiveLoginsImpl && - const DeepCollectionEquality() - .equals(other.userLogins, userLogins) && - (identical(other.activeUserLogin, activeUserLogin) || - other.activeUserLogin == activeUserLogin)); - } - - @JsonKey(ignore: true) - @override - int get hashCode => Object.hash(runtimeType, - const DeepCollectionEquality().hash(userLogins), activeUserLogin); - - @JsonKey(ignore: true) - @override - @pragma('vm:prefer-inline') - _$$ActiveLoginsImplCopyWith<_$ActiveLoginsImpl> get copyWith => - __$$ActiveLoginsImplCopyWithImpl<_$ActiveLoginsImpl>(this, _$identity); - - @override - Map toJson() { - return _$$ActiveLoginsImplToJson( - this, - ); - } -} - -abstract class _ActiveLogins implements ActiveLogins { - const factory _ActiveLogins( - {required final IList userLogins, - final Typed? activeUserLogin}) = _$ActiveLoginsImpl; - - factory _ActiveLogins.fromJson(Map json) = - _$ActiveLoginsImpl.fromJson; - - @override // The list of current logged in accounts - IList get userLogins; - @override // The current selected account indexed by master record key - Typed? get activeUserLogin; - @override - @JsonKey(ignore: true) - _$$ActiveLoginsImplCopyWith<_$ActiveLoginsImpl> get copyWith => - throw _privateConstructorUsedError; -} diff --git a/lib/entities/user_login.g.dart b/lib/local_account_manager/account_repository/user_login.g.dart similarity index 62% rename from lib/entities/user_login.g.dart rename to lib/local_account_manager/account_repository/user_login.g.dart index a2b2143..267fc55 100644 --- a/lib/entities/user_login.g.dart +++ b/lib/local_account_manager/account_repository/user_login.g.dart @@ -24,20 +24,3 @@ Map _$$UserLoginImplToJson(_$UserLoginImpl instance) => 'account_record_info': instance.accountRecordInfo.toJson(), 'last_active': instance.lastActive.toJson(), }; - -_$ActiveLoginsImpl _$$ActiveLoginsImplFromJson(Map json) => - _$ActiveLoginsImpl( - userLogins: IList.fromJson( - json['user_logins'], (value) => UserLogin.fromJson(value)), - activeUserLogin: json['active_user_login'] == null - ? null - : Typed.fromJson(json['active_user_login']), - ); - -Map _$$ActiveLoginsImplToJson(_$ActiveLoginsImpl instance) => - { - 'user_logins': instance.userLogins.toJson( - (value) => value.toJson(), - ), - 'active_user_login': instance.activeUserLogin?.toJson(), - }; diff --git a/lib/local_account_manager/active_user_login_cubit/active_user_login_cubit.dart b/lib/local_account_manager/active_user_login_cubit/active_user_login_cubit.dart new file mode 100644 index 0000000..b8d4dcf --- /dev/null +++ b/lib/local_account_manager/active_user_login_cubit/active_user_login_cubit.dart @@ -0,0 +1,42 @@ +import 'dart:async'; + +import 'package:bloc/bloc.dart'; + +import '../../veilid_support/veilid_support.dart'; +import '../account_repository/account_repository.dart'; + +part 'active_user_login_state.dart'; + +class ActiveUserLoginCubit extends Cubit { + ActiveUserLoginCubit({required AccountRepository accountRepository}) + : _accountRepository = accountRepository, + super(null) { + // Subscribe to streams + _initAccountRepositorySubscription(); + } + + void _initAccountRepositorySubscription() { + _accountRepositorySubscription = + _accountRepository.changes().listen((change) { + switch (change) { + case AccountRepositoryChange.activeUserLogin: + emit(_accountRepository.getActiveUserLogin()); + break; + // Ignore these + case AccountRepositoryChange.localAccounts: + case AccountRepositoryChange.userLogins: + break; + } + }); + } + + @override + Future close() async { + await super.close(); + await _accountRepositorySubscription.cancel(); + } + + final AccountRepository _accountRepository; + late final StreamSubscription + _accountRepositorySubscription; +} diff --git a/lib/local_account_manager/active_user_login_cubit/active_user_login_state.dart b/lib/local_account_manager/active_user_login_cubit/active_user_login_state.dart new file mode 100644 index 0000000..098e7a4 --- /dev/null +++ b/lib/local_account_manager/active_user_login_cubit/active_user_login_state.dart @@ -0,0 +1,3 @@ +part of 'active_user_login_cubit.dart'; + +typedef ActiveUserLoginState = TypedKey?; diff --git a/lib/local_account_manager/local_account_manager.dart b/lib/local_account_manager/local_account_manager.dart new file mode 100644 index 0000000..2b0a87f --- /dev/null +++ b/lib/local_account_manager/local_account_manager.dart @@ -0,0 +1,3 @@ +export 'account_repository/account_repository.dart'; +export 'local_accounts_cubit/local_accounts_cubit.dart'; +export 'user_logins_cubit/user_logins_cubit.dart'; diff --git a/lib/local_account_manager/local_accounts_cubit/local_accounts_cubit.dart b/lib/local_account_manager/local_accounts_cubit/local_accounts_cubit.dart new file mode 100644 index 0000000..aa54d6c --- /dev/null +++ b/lib/local_account_manager/local_accounts_cubit/local_accounts_cubit.dart @@ -0,0 +1,42 @@ +import 'dart:async'; + +import 'package:bloc/bloc.dart'; +import 'package:fast_immutable_collections/fast_immutable_collections.dart'; + +import '../account_repository/account_repository.dart'; + +part 'local_accounts_state.dart'; + +class LocalAccountsCubit extends Cubit { + LocalAccountsCubit({required AccountRepository accountRepository}) + : _accountRepository = accountRepository, + super(LocalAccountsState()) { + // Subscribe to streams + _initAccountRepositorySubscription(); + } + + void _initAccountRepositorySubscription() { + _accountRepositorySubscription = + _accountRepository.changes().listen((change) { + switch (change) { + case AccountRepositoryChange.localAccounts: + emit(_accountRepository.getLocalAccounts()); + break; + // Ignore these + case AccountRepositoryChange.userLogins: + case AccountRepositoryChange.activeUserLogin: + break; + } + }); + } + + @override + Future close() async { + await super.close(); + await _accountRepositorySubscription.cancel(); + } + + final AccountRepository _accountRepository; + late final StreamSubscription + _accountRepositorySubscription; +} diff --git a/lib/local_account_manager/local_accounts_cubit/local_accounts_state.dart b/lib/local_account_manager/local_accounts_cubit/local_accounts_state.dart new file mode 100644 index 0000000..13950c3 --- /dev/null +++ b/lib/local_account_manager/local_accounts_cubit/local_accounts_state.dart @@ -0,0 +1,3 @@ +part of 'local_accounts_cubit.dart'; + +typedef LocalAccountsState = IList; diff --git a/lib/local_account_manager/user_logins_cubit/user_logins_cubit.dart b/lib/local_account_manager/user_logins_cubit/user_logins_cubit.dart new file mode 100644 index 0000000..e8d9bef --- /dev/null +++ b/lib/local_account_manager/user_logins_cubit/user_logins_cubit.dart @@ -0,0 +1,42 @@ +import 'dart:async'; + +import 'package:bloc/bloc.dart'; +import 'package:fast_immutable_collections/fast_immutable_collections.dart'; + +import '../account_repository/account_repository.dart'; + +part 'user_logins_state.dart'; + +class UserLoginsCubit extends Cubit { + UserLoginsCubit({required AccountRepository accountRepository}) + : _accountRepository = accountRepository, + super(UserLoginsState()) { + // Subscribe to streams + _initAccountRepositorySubscription(); + } + + void _initAccountRepositorySubscription() { + _accountRepositorySubscription = + _accountRepository.changes().listen((change) { + switch (change) { + case AccountRepositoryChange.userLogins: + emit(_accountRepository.getUserLogins()); + break; + // Ignore these + case AccountRepositoryChange.localAccounts: + case AccountRepositoryChange.activeUserLogin: + break; + } + }); + } + + @override + Future close() async { + await super.close(); + await _accountRepositorySubscription.cancel(); + } + + final AccountRepository _accountRepository; + late final StreamSubscription + _accountRepositorySubscription; +} diff --git a/lib/local_account_manager/user_logins_cubit/user_logins_state.dart b/lib/local_account_manager/user_logins_cubit/user_logins_state.dart new file mode 100644 index 0000000..27dec5c --- /dev/null +++ b/lib/local_account_manager/user_logins_cubit/user_logins_state.dart @@ -0,0 +1,3 @@ +part of 'user_logins_cubit.dart'; + +typedef UserLoginsState = IList; diff --git a/lib/main.dart b/lib/main.dart index 3644eab..7cd828c 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -4,14 +4,16 @@ import 'dart:io'; import 'package:ansicolor/ansicolor.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_translate/flutter_translate.dart'; import 'package:intl/date_symbol_data_local.dart'; import 'app.dart'; -import 'providers/window_control.dart'; +import 'old_to_refactor/providers/window_control.dart'; +import 'theme/theme.dart'; import 'tools/tools.dart'; -import 'veilid_init.dart'; +import 'init.dart'; + +const String appName = "VeilidChat"; void main() async { // Disable all debugprints in release mode @@ -34,8 +36,8 @@ void main() async { // Prepare theme WidgetsFlutterBinding.ensureInitialized(); - final themeService = await ThemeService.instance; - final initTheme = themeService.initial; + final themeRepository = await ThemeRepository.instance; + final themeData = themeRepository.themeData(); // Manage window on desktop platforms await WindowControl.initialize(); @@ -50,9 +52,7 @@ void main() async { // Run the app // Hot reloads will only restart this part, not Veilid - runApp(ProviderScope( - observers: const [StateLogger()], - child: LocalizedApp(delegate, VeilidChatApp(theme: initTheme)))); + runApp(LocalizedApp(delegate, VeilidChatApp(themeData: themeData))); }, (error, stackTrace) { log.error('Dart Runtime: {$error}\n{$stackTrace}'); }); diff --git a/lib/components/account_bubble.dart b/lib/old_to_refactor/components/account_bubble.dart similarity index 100% rename from lib/components/account_bubble.dart rename to lib/old_to_refactor/components/account_bubble.dart diff --git a/lib/components/bottom_sheet_action_button.dart b/lib/old_to_refactor/components/bottom_sheet_action_button.dart similarity index 100% rename from lib/components/bottom_sheet_action_button.dart rename to lib/old_to_refactor/components/bottom_sheet_action_button.dart diff --git a/lib/components/chat_component.dart b/lib/old_to_refactor/components/chat_component.dart similarity index 100% rename from lib/components/chat_component.dart rename to lib/old_to_refactor/components/chat_component.dart diff --git a/lib/components/chat_single_contact_item_widget.dart b/lib/old_to_refactor/components/chat_single_contact_item_widget.dart similarity index 99% rename from lib/components/chat_single_contact_item_widget.dart rename to lib/old_to_refactor/components/chat_single_contact_item_widget.dart index f9cc102..660b415 100644 --- a/lib/components/chat_single_contact_item_widget.dart +++ b/lib/old_to_refactor/components/chat_single_contact_item_widget.dart @@ -6,7 +6,7 @@ import 'package:flutter_translate/flutter_translate.dart'; import '../proto/proto.dart' as proto; import '../providers/account.dart'; import '../providers/chat.dart'; -import '../tools/theme_service.dart'; +import '../theme/theme.dart'; class ChatSingleContactItemWidget extends ConsumerWidget { const ChatSingleContactItemWidget({required this.contact, super.key}); diff --git a/lib/components/chat_single_contact_list_widget.dart b/lib/old_to_refactor/components/chat_single_contact_list_widget.dart similarity index 100% rename from lib/components/chat_single_contact_list_widget.dart rename to lib/old_to_refactor/components/chat_single_contact_list_widget.dart diff --git a/lib/components/contact_invitation_display.dart b/lib/old_to_refactor/components/contact_invitation_display.dart similarity index 100% rename from lib/components/contact_invitation_display.dart rename to lib/old_to_refactor/components/contact_invitation_display.dart diff --git a/lib/components/contact_invitation_item_widget.dart b/lib/old_to_refactor/components/contact_invitation_item_widget.dart similarity index 100% rename from lib/components/contact_invitation_item_widget.dart rename to lib/old_to_refactor/components/contact_invitation_item_widget.dart diff --git a/lib/components/contact_invitation_list_widget.dart b/lib/old_to_refactor/components/contact_invitation_list_widget.dart similarity index 100% rename from lib/components/contact_invitation_list_widget.dart rename to lib/old_to_refactor/components/contact_invitation_list_widget.dart diff --git a/lib/components/contact_item_widget.dart b/lib/old_to_refactor/components/contact_item_widget.dart similarity index 99% rename from lib/components/contact_item_widget.dart rename to lib/old_to_refactor/components/contact_item_widget.dart index 1a4cb46..ed57997 100644 --- a/lib/components/contact_item_widget.dart +++ b/lib/old_to_refactor/components/contact_item_widget.dart @@ -9,7 +9,7 @@ import '../pages/main_pager/main_pager.dart'; import '../providers/account.dart'; import '../providers/chat.dart'; import '../providers/contact.dart'; -import '../tools/theme_service.dart'; +import '../theme/theme.dart'; class ContactItemWidget extends ConsumerWidget { const ContactItemWidget({required this.contact, super.key}); diff --git a/lib/components/contact_list_widget.dart b/lib/old_to_refactor/components/contact_list_widget.dart similarity index 100% rename from lib/components/contact_list_widget.dart rename to lib/old_to_refactor/components/contact_list_widget.dart diff --git a/lib/components/default_app_bar.dart b/lib/old_to_refactor/components/default_app_bar.dart similarity index 100% rename from lib/components/default_app_bar.dart rename to lib/old_to_refactor/components/default_app_bar.dart diff --git a/lib/components/empty_chat_list_widget.dart b/lib/old_to_refactor/components/empty_chat_list_widget.dart similarity index 100% rename from lib/components/empty_chat_list_widget.dart rename to lib/old_to_refactor/components/empty_chat_list_widget.dart diff --git a/lib/components/empty_chat_widget.dart b/lib/old_to_refactor/components/empty_chat_widget.dart similarity index 100% rename from lib/components/empty_chat_widget.dart rename to lib/old_to_refactor/components/empty_chat_widget.dart diff --git a/lib/components/empty_contact_list_widget.dart b/lib/old_to_refactor/components/empty_contact_list_widget.dart similarity index 100% rename from lib/components/empty_contact_list_widget.dart rename to lib/old_to_refactor/components/empty_contact_list_widget.dart diff --git a/lib/components/enter_password.dart b/lib/old_to_refactor/components/enter_password.dart similarity index 100% rename from lib/components/enter_password.dart rename to lib/old_to_refactor/components/enter_password.dart diff --git a/lib/components/enter_pin.dart b/lib/old_to_refactor/components/enter_pin.dart similarity index 100% rename from lib/components/enter_pin.dart rename to lib/old_to_refactor/components/enter_pin.dart diff --git a/lib/components/invite_dialog.dart b/lib/old_to_refactor/components/invite_dialog.dart similarity index 100% rename from lib/components/invite_dialog.dart rename to lib/old_to_refactor/components/invite_dialog.dart diff --git a/lib/components/no_conversation_widget.dart b/lib/old_to_refactor/components/no_conversation_widget.dart similarity index 100% rename from lib/components/no_conversation_widget.dart rename to lib/old_to_refactor/components/no_conversation_widget.dart diff --git a/lib/components/paste_invite_dialog.dart b/lib/old_to_refactor/components/paste_invite_dialog.dart similarity index 100% rename from lib/components/paste_invite_dialog.dart rename to lib/old_to_refactor/components/paste_invite_dialog.dart diff --git a/lib/components/profile_widget.dart b/lib/old_to_refactor/components/profile_widget.dart similarity index 100% rename from lib/components/profile_widget.dart rename to lib/old_to_refactor/components/profile_widget.dart diff --git a/lib/components/scan_invite_dialog.dart b/lib/old_to_refactor/components/scan_invite_dialog.dart similarity index 100% rename from lib/components/scan_invite_dialog.dart rename to lib/old_to_refactor/components/scan_invite_dialog.dart diff --git a/lib/components/send_invite_dialog.dart b/lib/old_to_refactor/components/send_invite_dialog.dart similarity index 100% rename from lib/components/send_invite_dialog.dart rename to lib/old_to_refactor/components/send_invite_dialog.dart diff --git a/lib/components/signal_strength_meter.dart b/lib/old_to_refactor/components/signal_strength_meter.dart similarity index 100% rename from lib/components/signal_strength_meter.dart rename to lib/old_to_refactor/components/signal_strength_meter.dart diff --git a/lib/entities/entities.dart b/lib/old_to_refactor/entities/entities.dart similarity index 100% rename from lib/entities/entities.dart rename to lib/old_to_refactor/entities/entities.dart diff --git a/lib/entities/preferences.dart b/lib/old_to_refactor/entities/preferences.dart similarity index 55% rename from lib/entities/preferences.dart rename to lib/old_to_refactor/entities/preferences.dart index c7d7a4f..f0e3e82 100644 --- a/lib/entities/preferences.dart +++ b/lib/old_to_refactor/entities/preferences.dart @@ -4,19 +4,6 @@ import 'package:freezed_annotation/freezed_annotation.dart'; part 'preferences.freezed.dart'; part 'preferences.g.dart'; -// Theme supports light and dark mode, optionally selected by the -// operating system -enum BrightnessPreference { - system, - light, - dark; - - factory BrightnessPreference.fromJson(dynamic j) => - BrightnessPreference.values.byName((j as String).toCamelCase()); - - String toJson() => name.toPascalCase(); -} - // Lock preference changes how frequently the messenger locks its // interface and requires the identitySecretKey to be entered (pin/password/etc) @freezed @@ -31,28 +18,6 @@ class LockPreference with _$LockPreference { _$LockPreferenceFromJson(json as Map); } -// Theme supports multiple color variants based on 'Radix' -enum ColorPreference { - // Radix Colors - scarlet, - babydoll, - vapor, - gold, - garden, - forest, - arctic, - lapis, - eggplant, - lime, - grim, - // Accessible Colors - contrast; - - factory ColorPreference.fromJson(dynamic j) => - ColorPreference.values.byName((j as String).toCamelCase()); - String toJson() => name.toPascalCase(); -} - // Theme supports multiple translations enum LanguagePreference { englishUS; @@ -62,18 +27,6 @@ enum LanguagePreference { String toJson() => name.toPascalCase(); } -@freezed -class ThemePreferences with _$ThemePreferences { - const factory ThemePreferences({ - required BrightnessPreference brightnessPreference, - required ColorPreference colorPreference, - required double displayScale, - }) = _ThemePreferences; - - factory ThemePreferences.fromJson(dynamic json) => - _$ThemePreferencesFromJson(json as Map); -} - // Preferences are stored in a table locally and globally affect all // accounts imported/added and the app in general @freezed diff --git a/lib/entities/preferences.freezed.dart b/lib/old_to_refactor/entities/preferences.freezed.dart similarity index 64% rename from lib/entities/preferences.freezed.dart rename to lib/old_to_refactor/entities/preferences.freezed.dart index 0e951e0..09484e2 100644 --- a/lib/entities/preferences.freezed.dart +++ b/lib/old_to_refactor/entities/preferences.freezed.dart @@ -199,192 +199,6 @@ abstract class _LockPreference implements LockPreference { throw _privateConstructorUsedError; } -ThemePreferences _$ThemePreferencesFromJson(Map json) { - return _ThemePreferences.fromJson(json); -} - -/// @nodoc -mixin _$ThemePreferences { - BrightnessPreference get brightnessPreference => - throw _privateConstructorUsedError; - ColorPreference get colorPreference => throw _privateConstructorUsedError; - double get displayScale => throw _privateConstructorUsedError; - - Map toJson() => throw _privateConstructorUsedError; - @JsonKey(ignore: true) - $ThemePreferencesCopyWith get copyWith => - throw _privateConstructorUsedError; -} - -/// @nodoc -abstract class $ThemePreferencesCopyWith<$Res> { - factory $ThemePreferencesCopyWith( - ThemePreferences value, $Res Function(ThemePreferences) then) = - _$ThemePreferencesCopyWithImpl<$Res, ThemePreferences>; - @useResult - $Res call( - {BrightnessPreference brightnessPreference, - ColorPreference colorPreference, - double displayScale}); -} - -/// @nodoc -class _$ThemePreferencesCopyWithImpl<$Res, $Val extends ThemePreferences> - implements $ThemePreferencesCopyWith<$Res> { - _$ThemePreferencesCopyWithImpl(this._value, this._then); - - // ignore: unused_field - final $Val _value; - // ignore: unused_field - final $Res Function($Val) _then; - - @pragma('vm:prefer-inline') - @override - $Res call({ - Object? brightnessPreference = null, - Object? colorPreference = null, - Object? displayScale = null, - }) { - return _then(_value.copyWith( - brightnessPreference: null == brightnessPreference - ? _value.brightnessPreference - : brightnessPreference // ignore: cast_nullable_to_non_nullable - as BrightnessPreference, - colorPreference: null == colorPreference - ? _value.colorPreference - : colorPreference // ignore: cast_nullable_to_non_nullable - as ColorPreference, - displayScale: null == displayScale - ? _value.displayScale - : displayScale // ignore: cast_nullable_to_non_nullable - as double, - ) as $Val); - } -} - -/// @nodoc -abstract class _$$ThemePreferencesImplCopyWith<$Res> - implements $ThemePreferencesCopyWith<$Res> { - factory _$$ThemePreferencesImplCopyWith(_$ThemePreferencesImpl value, - $Res Function(_$ThemePreferencesImpl) then) = - __$$ThemePreferencesImplCopyWithImpl<$Res>; - @override - @useResult - $Res call( - {BrightnessPreference brightnessPreference, - ColorPreference colorPreference, - double displayScale}); -} - -/// @nodoc -class __$$ThemePreferencesImplCopyWithImpl<$Res> - extends _$ThemePreferencesCopyWithImpl<$Res, _$ThemePreferencesImpl> - implements _$$ThemePreferencesImplCopyWith<$Res> { - __$$ThemePreferencesImplCopyWithImpl(_$ThemePreferencesImpl _value, - $Res Function(_$ThemePreferencesImpl) _then) - : super(_value, _then); - - @pragma('vm:prefer-inline') - @override - $Res call({ - Object? brightnessPreference = null, - Object? colorPreference = null, - Object? displayScale = null, - }) { - return _then(_$ThemePreferencesImpl( - brightnessPreference: null == brightnessPreference - ? _value.brightnessPreference - : brightnessPreference // ignore: cast_nullable_to_non_nullable - as BrightnessPreference, - colorPreference: null == colorPreference - ? _value.colorPreference - : colorPreference // ignore: cast_nullable_to_non_nullable - as ColorPreference, - displayScale: null == displayScale - ? _value.displayScale - : displayScale // ignore: cast_nullable_to_non_nullable - as double, - )); - } -} - -/// @nodoc -@JsonSerializable() -class _$ThemePreferencesImpl implements _ThemePreferences { - const _$ThemePreferencesImpl( - {required this.brightnessPreference, - required this.colorPreference, - required this.displayScale}); - - factory _$ThemePreferencesImpl.fromJson(Map json) => - _$$ThemePreferencesImplFromJson(json); - - @override - final BrightnessPreference brightnessPreference; - @override - final ColorPreference colorPreference; - @override - final double displayScale; - - @override - String toString() { - return 'ThemePreferences(brightnessPreference: $brightnessPreference, colorPreference: $colorPreference, displayScale: $displayScale)'; - } - - @override - bool operator ==(Object other) { - return identical(this, other) || - (other.runtimeType == runtimeType && - other is _$ThemePreferencesImpl && - (identical(other.brightnessPreference, brightnessPreference) || - other.brightnessPreference == brightnessPreference) && - (identical(other.colorPreference, colorPreference) || - other.colorPreference == colorPreference) && - (identical(other.displayScale, displayScale) || - other.displayScale == displayScale)); - } - - @JsonKey(ignore: true) - @override - int get hashCode => Object.hash( - runtimeType, brightnessPreference, colorPreference, displayScale); - - @JsonKey(ignore: true) - @override - @pragma('vm:prefer-inline') - _$$ThemePreferencesImplCopyWith<_$ThemePreferencesImpl> get copyWith => - __$$ThemePreferencesImplCopyWithImpl<_$ThemePreferencesImpl>( - this, _$identity); - - @override - Map toJson() { - return _$$ThemePreferencesImplToJson( - this, - ); - } -} - -abstract class _ThemePreferences implements ThemePreferences { - const factory _ThemePreferences( - {required final BrightnessPreference brightnessPreference, - required final ColorPreference colorPreference, - required final double displayScale}) = _$ThemePreferencesImpl; - - factory _ThemePreferences.fromJson(Map json) = - _$ThemePreferencesImpl.fromJson; - - @override - BrightnessPreference get brightnessPreference; - @override - ColorPreference get colorPreference; - @override - double get displayScale; - @override - @JsonKey(ignore: true) - _$$ThemePreferencesImplCopyWith<_$ThemePreferencesImpl> get copyWith => - throw _privateConstructorUsedError; -} - Preferences _$PreferencesFromJson(Map json) { return _Preferences.fromJson(json); } @@ -412,7 +226,6 @@ abstract class $PreferencesCopyWith<$Res> { LanguagePreference language, LockPreference locking}); - $ThemePreferencesCopyWith<$Res> get themePreferences; $LockPreferenceCopyWith<$Res> get locking; } @@ -429,12 +242,12 @@ class _$PreferencesCopyWithImpl<$Res, $Val extends Preferences> @pragma('vm:prefer-inline') @override $Res call({ - Object? themePreferences = null, + Object? themePreferences = freezed, Object? language = null, Object? locking = null, }) { return _then(_value.copyWith( - themePreferences: null == themePreferences + themePreferences: freezed == themePreferences ? _value.themePreferences : themePreferences // ignore: cast_nullable_to_non_nullable as ThemePreferences, @@ -449,14 +262,6 @@ class _$PreferencesCopyWithImpl<$Res, $Val extends Preferences> ) as $Val); } - @override - @pragma('vm:prefer-inline') - $ThemePreferencesCopyWith<$Res> get themePreferences { - return $ThemePreferencesCopyWith<$Res>(_value.themePreferences, (value) { - return _then(_value.copyWith(themePreferences: value) as $Val); - }); - } - @override @pragma('vm:prefer-inline') $LockPreferenceCopyWith<$Res> get locking { @@ -479,8 +284,6 @@ abstract class _$$PreferencesImplCopyWith<$Res> LanguagePreference language, LockPreference locking}); - @override - $ThemePreferencesCopyWith<$Res> get themePreferences; @override $LockPreferenceCopyWith<$Res> get locking; } @@ -496,12 +299,12 @@ class __$$PreferencesImplCopyWithImpl<$Res> @pragma('vm:prefer-inline') @override $Res call({ - Object? themePreferences = null, + Object? themePreferences = freezed, Object? language = null, Object? locking = null, }) { return _then(_$PreferencesImpl( - themePreferences: null == themePreferences + themePreferences: freezed == themePreferences ? _value.themePreferences : themePreferences // ignore: cast_nullable_to_non_nullable as ThemePreferences, @@ -545,8 +348,8 @@ class _$PreferencesImpl implements _Preferences { return identical(this, other) || (other.runtimeType == runtimeType && other is _$PreferencesImpl && - (identical(other.themePreferences, themePreferences) || - other.themePreferences == themePreferences) && + const DeepCollectionEquality() + .equals(other.themePreferences, themePreferences) && (identical(other.language, language) || other.language == language) && (identical(other.locking, locking) || other.locking == locking)); @@ -554,8 +357,8 @@ class _$PreferencesImpl implements _Preferences { @JsonKey(ignore: true) @override - int get hashCode => - Object.hash(runtimeType, themePreferences, language, locking); + int get hashCode => Object.hash(runtimeType, + const DeepCollectionEquality().hash(themePreferences), language, locking); @JsonKey(ignore: true) @override diff --git a/lib/entities/preferences.g.dart b/lib/old_to_refactor/entities/preferences.g.dart similarity index 100% rename from lib/entities/preferences.g.dart rename to lib/old_to_refactor/entities/preferences.g.dart diff --git a/lib/managers/contact_list_manager.dart b/lib/old_to_refactor/managers/contact_list_manager.dart similarity index 100% rename from lib/managers/contact_list_manager.dart rename to lib/old_to_refactor/managers/contact_list_manager.dart diff --git a/lib/managers/valid_contact_invitation.dart b/lib/old_to_refactor/managers/valid_contact_invitation.dart similarity index 100% rename from lib/managers/valid_contact_invitation.dart rename to lib/old_to_refactor/managers/valid_contact_invitation.dart diff --git a/lib/pages/chat_only.dart b/lib/old_to_refactor/pages/chat_only.dart similarity index 95% rename from lib/pages/chat_only.dart rename to lib/old_to_refactor/pages/chat_only.dart index 2dc57d2..ad81b4c 100644 --- a/lib/pages/chat_only.dart +++ b/lib/old_to_refactor/pages/chat_only.dart @@ -4,7 +4,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../providers/window_control.dart'; import 'home.dart'; -class ChatOnlyPage extends ConsumerStatefulWidget { +class ChatOnlyPage extends StatefulWidget { const ChatOnlyPage({super.key}); @override diff --git a/lib/pages/developer.dart b/lib/old_to_refactor/pages/developer.dart similarity index 98% rename from lib/pages/developer.dart rename to lib/old_to_refactor/pages/developer.dart index da78c9c..6a7bd91 100644 --- a/lib/pages/developer.dart +++ b/lib/old_to_refactor/pages/developer.dart @@ -13,8 +13,8 @@ import 'package:loggy/loggy.dart'; import 'package:quickalert/quickalert.dart'; import 'package:xterm/xterm.dart'; -import '../tools/tools.dart'; -import '../veilid_support/veilid_support.dart'; +import '../../tools/tools.dart'; +import '../../veilid_support/veilid_support.dart'; final globalDebugTerminal = Terminal( maxLines: 50000, @@ -25,7 +25,7 @@ const kDefaultTerminalStyle = TerminalStyle( // height: 1.2, fontFamily: 'Source Code Pro'); -class DeveloperPage extends ConsumerStatefulWidget { +class DeveloperPage extends StatefulWidget { const DeveloperPage({super.key}); @override diff --git a/lib/pages/edit_account.dart b/lib/old_to_refactor/pages/edit_account.dart similarity index 100% rename from lib/pages/edit_account.dart rename to lib/old_to_refactor/pages/edit_account.dart diff --git a/lib/pages/edit_contact.dart b/lib/old_to_refactor/pages/edit_contact.dart similarity index 100% rename from lib/pages/edit_contact.dart rename to lib/old_to_refactor/pages/edit_contact.dart diff --git a/lib/pages/home.dart b/lib/old_to_refactor/pages/home.dart similarity index 95% rename from lib/pages/home.dart rename to lib/old_to_refactor/pages/home.dart index 408aa86..78b09aa 100644 --- a/lib/pages/home.dart +++ b/lib/old_to_refactor/pages/home.dart @@ -6,22 +6,22 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_translate/flutter_translate.dart'; import 'package:go_router/go_router.dart'; -import '../proto/proto.dart' as proto; -import '../components/chat_component.dart'; -import '../components/empty_chat_widget.dart'; -import '../components/profile_widget.dart'; -import '../entities/local_account.dart'; +import '../../proto/proto.dart' as proto; +import '../../components/chat_component.dart'; +import '../../components/empty_chat_widget.dart'; +import '../../components/profile_widget.dart'; +import '../../entities/local_account.dart'; import '../providers/account.dart'; import '../providers/chat.dart'; import '../providers/contact.dart'; -import '../providers/local_accounts.dart'; +import '../../local_accounts/local_accounts.dart'; import '../providers/logins.dart'; import '../providers/window_control.dart'; -import '../tools/tools.dart'; -import '../veilid_support/veilid_support.dart'; +import '../../tools/tools.dart'; +import '../../veilid_support/veilid_support.dart'; import 'main_pager/main_pager.dart'; -class HomePage extends ConsumerStatefulWidget { +class HomePage extends StatefulWidget { const HomePage({super.key}); @override diff --git a/lib/pages/index.dart b/lib/old_to_refactor/pages/index.dart similarity index 87% rename from lib/pages/index.dart rename to lib/old_to_refactor/pages/index.dart index 8a53316..3690d70 100644 --- a/lib/pages/index.dart +++ b/lib/old_to_refactor/pages/index.dart @@ -1,17 +1,12 @@ import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:radix_colors/radix_colors.dart'; -import '../providers/window_control.dart'; - -class IndexPage extends ConsumerWidget { +class IndexPage extends StatelessWidget { const IndexPage({super.key}); @override - Widget build(BuildContext context, WidgetRef ref) { - ref.watch(windowControlProvider); - + Widget build(BuildContext context) { final theme = Theme.of(context); final textTheme = theme.textTheme; final monoTextStyle = textTheme.labelSmall! diff --git a/lib/pages/main_pager/account.dart b/lib/old_to_refactor/pages/main_pager/account.dart similarity index 89% rename from lib/pages/main_pager/account.dart rename to lib/old_to_refactor/pages/main_pager/account.dart index 4c3e56d..ca6aa1d 100644 --- a/lib/pages/main_pager/account.dart +++ b/lib/old_to_refactor/pages/main_pager/account.dart @@ -7,15 +7,15 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_translate/flutter_translate.dart'; -import '../../components/contact_invitation_list_widget.dart'; -import '../../components/contact_list_widget.dart'; -import '../../entities/local_account.dart'; -import '../../proto/proto.dart' as proto; +import '../../../components/contact_invitation_list_widget.dart'; +import '../../../components/contact_list_widget.dart'; +import '../../../entities/local_account.dart'; +import '../../../proto/proto.dart' as proto; import '../../providers/contact.dart'; import '../../providers/contact_invite.dart'; -import '../../tools/theme_service.dart'; -import '../../tools/tools.dart'; -import '../../veilid_support/veilid_support.dart'; +import '../../../theme/theme.dart'; +import '../../../tools/tools.dart'; +import '../../../veilid_support/veilid_support.dart'; class AccountPage extends ConsumerStatefulWidget { const AccountPage({ diff --git a/lib/pages/main_pager/chats.dart b/lib/old_to_refactor/pages/main_pager/chats.dart similarity index 88% rename from lib/pages/main_pager/chats.dart rename to lib/old_to_refactor/pages/main_pager/chats.dart index e823dfd..c98d128 100644 --- a/lib/pages/main_pager/chats.dart +++ b/lib/old_to_refactor/pages/main_pager/chats.dart @@ -3,17 +3,17 @@ import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import '../../components/chat_single_contact_list_widget.dart'; -import '../../components/empty_chat_list_widget.dart'; -import '../../entities/local_account.dart'; -import '../../proto/proto.dart' as proto; +import '../../../components/chat_single_contact_list_widget.dart'; +import '../../../components/empty_chat_list_widget.dart'; +import '../../../entities/local_account.dart'; +import '../../../proto/proto.dart' as proto; import '../../providers/account.dart'; import '../../providers/chat.dart'; import '../../providers/contact.dart'; -import '../../providers/local_accounts.dart'; +import '../../../local_accounts/local_accounts.dart'; import '../../providers/logins.dart'; -import '../../tools/tools.dart'; -import '../../veilid_support/veilid_support.dart'; +import '../../../tools/tools.dart'; +import '../../../veilid_support/veilid_support.dart'; class ChatsPage extends ConsumerStatefulWidget { const ChatsPage({super.key}); diff --git a/lib/pages/main_pager/main_pager.dart b/lib/old_to_refactor/pages/main_pager/main_pager.dart similarity index 96% rename from lib/pages/main_pager/main_pager.dart rename to lib/old_to_refactor/pages/main_pager/main_pager.dart index 7265285..fd3ef14 100644 --- a/lib/pages/main_pager/main_pager.dart +++ b/lib/old_to_refactor/pages/main_pager/main_pager.dart @@ -13,14 +13,14 @@ import 'package:preload_page_view/preload_page_view.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/paste_invite_dialog.dart'; -import '../../components/scan_invite_dialog.dart'; -import '../../components/send_invite_dialog.dart'; -import '../../entities/local_account.dart'; -import '../../proto/proto.dart' as proto; -import '../../tools/tools.dart'; -import '../../veilid_support/veilid_support.dart'; +import '../../../components/bottom_sheet_action_button.dart'; +import '../../../components/paste_invite_dialog.dart'; +import '../../../components/scan_invite_dialog.dart'; +import '../../../components/send_invite_dialog.dart'; +import '../../../entities/local_account.dart'; +import '../../../proto/proto.dart' as proto; +import '../../../tools/tools.dart'; +import '../../../veilid_support/veilid_support.dart'; import 'account.dart'; import 'chats.dart'; diff --git a/lib/pages/new_account.dart b/lib/old_to_refactor/pages/new_account.dart similarity index 94% rename from lib/pages/new_account.dart rename to lib/old_to_refactor/pages/new_account.dart index 3ef1b6d..18d3963 100644 --- a/lib/pages/new_account.dart +++ b/lib/old_to_refactor/pages/new_account.dart @@ -7,16 +7,16 @@ import 'package:flutter_translate/flutter_translate.dart'; import 'package:form_builder_validators/form_builder_validators.dart'; import 'package:go_router/go_router.dart'; -import '../components/default_app_bar.dart'; -import '../components/signal_strength_meter.dart'; -import '../entities/entities.dart'; -import '../providers/local_accounts.dart'; +import '../../components/default_app_bar.dart'; +import '../../components/signal_strength_meter.dart'; +import '../../entities/entities.dart'; +import '../../local_accounts/local_accounts.dart'; import '../providers/logins.dart'; import '../providers/window_control.dart'; -import '../tools/tools.dart'; -import '../veilid_support/veilid_support.dart'; +import '../../tools/tools.dart'; +import '../../veilid_support/veilid_support.dart'; -class NewAccountPage extends ConsumerStatefulWidget { +class NewAccountPage extends StatefulWidget { const NewAccountPage({super.key}); @override diff --git a/lib/pages/settings.dart b/lib/old_to_refactor/pages/settings.dart similarity index 95% rename from lib/pages/settings.dart rename to lib/old_to_refactor/pages/settings.dart index dfbb816..fb40352 100644 --- a/lib/pages/settings.dart +++ b/lib/old_to_refactor/pages/settings.dart @@ -6,13 +6,13 @@ import 'package:flutter_form_builder/flutter_form_builder.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_translate/flutter_translate.dart'; -import '../components/default_app_bar.dart'; -import '../components/signal_strength_meter.dart'; -import '../entities/preferences.dart'; +import '../../components/default_app_bar.dart'; +import '../../components/signal_strength_meter.dart'; +import '../../entities/preferences.dart'; import '../providers/window_control.dart'; -import '../tools/tools.dart'; +import '../../tools/tools.dart'; -class SettingsPage extends ConsumerStatefulWidget { +class SettingsPage extends StatefulWidget { const SettingsPage({super.key}); @override diff --git a/lib/providers/account.dart b/lib/old_to_refactor/providers/account.dart similarity index 94% rename from lib/providers/account.dart rename to lib/old_to_refactor/providers/account.dart index 93f1ce9..8da8d58 100644 --- a/lib/providers/account.dart +++ b/lib/old_to_refactor/providers/account.dart @@ -1,11 +1,11 @@ import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; -import '../entities/local_account.dart'; -import '../entities/user_login.dart'; -import '../proto/proto.dart' as proto; -import '../veilid_support/veilid_support.dart'; -import 'local_accounts.dart'; +import '../../entities/local_account.dart'; +import '../../entities/user_login.dart'; +import '../../proto/proto.dart' as proto; +import '../../veilid_support/veilid_support.dart'; +import '../../local_accounts/local_accounts.dart'; import 'logins.dart'; part 'account.g.dart'; diff --git a/lib/providers/chat.dart b/lib/old_to_refactor/providers/chat.dart similarity index 97% rename from lib/providers/chat.dart rename to lib/old_to_refactor/providers/chat.dart index d64c4b9..ca1c086 100644 --- a/lib/providers/chat.dart +++ b/lib/old_to_refactor/providers/chat.dart @@ -2,9 +2,9 @@ import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; -import '../proto/proto.dart' as proto; +import '../../proto/proto.dart' as proto; -import '../veilid_support/veilid_support.dart'; +import '../../veilid_support/veilid_support.dart'; import 'account.dart'; part 'chat.g.dart'; diff --git a/lib/providers/connection_state.dart b/lib/old_to_refactor/providers/connection_state.dart similarity index 95% rename from lib/providers/connection_state.dart rename to lib/old_to_refactor/providers/connection_state.dart index c7360bc..1adf8f7 100644 --- a/lib/providers/connection_state.dart +++ b/lib/old_to_refactor/providers/connection_state.dart @@ -1,7 +1,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; -import '../veilid_support/veilid_support.dart'; +import '../../veilid_support/veilid_support.dart'; part 'connection_state.freezed.dart'; diff --git a/lib/providers/connection_state.freezed.dart b/lib/old_to_refactor/providers/connection_state.freezed.dart similarity index 100% rename from lib/providers/connection_state.freezed.dart rename to lib/old_to_refactor/providers/connection_state.freezed.dart diff --git a/lib/providers/contact.dart b/lib/old_to_refactor/providers/contact.dart similarity index 97% rename from lib/providers/contact.dart rename to lib/old_to_refactor/providers/contact.dart index c44b302..731d5a7 100644 --- a/lib/providers/contact.dart +++ b/lib/old_to_refactor/providers/contact.dart @@ -3,10 +3,10 @@ import 'dart:convert'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; -import '../proto/proto.dart' as proto; +import '../../proto/proto.dart' as proto; -import '../veilid_support/veilid_support.dart'; -import '../tools/tools.dart'; +import '../../veilid_support/veilid_support.dart'; +import '../../tools/tools.dart'; import 'account.dart'; import 'chat.dart'; diff --git a/lib/providers/contact_invitation_list_manager.dart b/lib/old_to_refactor/providers/contact_invitation_list_manager.dart similarity index 99% rename from lib/providers/contact_invitation_list_manager.dart rename to lib/old_to_refactor/providers/contact_invitation_list_manager.dart index 2058832..099bc26 100644 --- a/lib/providers/contact_invitation_list_manager.dart +++ b/lib/old_to_refactor/providers/contact_invitation_list_manager.dart @@ -4,10 +4,10 @@ import 'package:flutter/foundation.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:mutex/mutex.dart'; -import '../entities/entities.dart'; -import '../proto/proto.dart' as proto; -import '../tools/tools.dart'; -import '../veilid_support/veilid_support.dart'; +import '../../entities/entities.dart'; +import '../../proto/proto.dart' as proto; +import '../../tools/tools.dart'; +import '../../veilid_support/veilid_support.dart'; import 'account.dart'; part 'contact_invitation_list_manager.g.dart'; diff --git a/lib/providers/contact_invite.dart b/lib/old_to_refactor/providers/contact_invite.dart similarity index 90% rename from lib/providers/contact_invite.dart rename to lib/old_to_refactor/providers/contact_invite.dart index bdbb8f7..a8f5787 100644 --- a/lib/providers/contact_invite.dart +++ b/lib/old_to_refactor/providers/contact_invite.dart @@ -4,10 +4,10 @@ import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:fixnum/fixnum.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; -import '../entities/local_account.dart'; -import '../proto/proto.dart' as proto; -import '../tools/tools.dart'; -import '../veilid_support/veilid_support.dart'; +import '../../entities/local_account.dart'; +import '../../proto/proto.dart' as proto; +import '../../tools/tools.dart'; +import '../../veilid_support/veilid_support.dart'; import 'account.dart'; import 'conversation.dart'; diff --git a/lib/providers/conversation.dart b/lib/old_to_refactor/providers/conversation.dart similarity index 98% rename from lib/providers/conversation.dart rename to lib/old_to_refactor/providers/conversation.dart index bac8cda..638e1ec 100644 --- a/lib/providers/conversation.dart +++ b/lib/old_to_refactor/providers/conversation.dart @@ -7,11 +7,11 @@ import 'dart:convert'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; -import '../proto/proto.dart' as proto; +import '../../proto/proto.dart' as proto; -import '../tools/tools.dart'; -import '../veilid_init.dart'; -import '../veilid_support/veilid_support.dart'; +import '../../tools/tools.dart'; +import '../../veilid_init.dart'; +import '../../veilid_support/veilid_support.dart'; import 'account.dart'; import 'chat.dart'; import 'contact.dart'; diff --git a/lib/providers/window_control.dart b/lib/old_to_refactor/providers/window_control.dart similarity index 98% rename from lib/providers/window_control.dart rename to lib/old_to_refactor/providers/window_control.dart index 56116f1..b6dfb76 100644 --- a/lib/providers/window_control.dart +++ b/lib/old_to_refactor/providers/window_control.dart @@ -5,7 +5,7 @@ import 'package:flutter/services.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:window_manager/window_manager.dart'; -import '../tools/responsive.dart'; +import '../../tools/responsive.dart'; export 'package:window_manager/window_manager.dart' show TitleBarStyle; diff --git a/lib/processor.dart b/lib/processor.dart index be414b1..5cb7c89 100644 --- a/lib/processor.dart +++ b/lib/processor.dart @@ -2,7 +2,7 @@ import 'dart:async'; import 'package:veilid/veilid.dart'; -import 'providers/connection_state.dart'; +import 'old_to_refactor/providers/connection_state.dart'; import 'tools/tools.dart'; import 'veilid_support/src/config.dart'; import 'veilid_support/src/veilid_log.dart'; diff --git a/lib/providers/account.g.dart b/lib/providers/account.g.dart deleted file mode 100644 index c1b1d59..0000000 --- a/lib/providers/account.g.dart +++ /dev/null @@ -1,200 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'account.dart'; - -// ************************************************************************** -// RiverpodGenerator -// ************************************************************************** - -String _$fetchAccountInfoHash() => r'3d2e3b3ddce5158d03bceaf82cdb35bae000280c'; - -/// Copied from Dart SDK -class _SystemHash { - _SystemHash._(); - - static int combine(int hash, int value) { - // ignore: parameter_assignments - hash = 0x1fffffff & (hash + value); - // ignore: parameter_assignments - hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10)); - return hash ^ (hash >> 6); - } - - static int finish(int hash) { - // ignore: parameter_assignments - hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3)); - // ignore: parameter_assignments - hash = hash ^ (hash >> 11); - return 0x1fffffff & (hash + ((0x00003fff & hash) << 15)); - } -} - -/// Get an account from the identity key and if it is logged in and we -/// have its secret available, return the account record contents -/// -/// Copied from [fetchAccountInfo]. -@ProviderFor(fetchAccountInfo) -const fetchAccountInfoProvider = FetchAccountInfoFamily(); - -/// Get an account from the identity key and if it is logged in and we -/// have its secret available, return the account record contents -/// -/// Copied from [fetchAccountInfo]. -class FetchAccountInfoFamily extends Family> { - /// Get an account from the identity key and if it is logged in and we - /// have its secret available, return the account record contents - /// - /// Copied from [fetchAccountInfo]. - const FetchAccountInfoFamily(); - - /// Get an account from the identity key and if it is logged in and we - /// have its secret available, return the account record contents - /// - /// Copied from [fetchAccountInfo]. - FetchAccountInfoProvider call({ - required Typed accountMasterRecordKey, - }) { - return FetchAccountInfoProvider( - accountMasterRecordKey: accountMasterRecordKey, - ); - } - - @override - FetchAccountInfoProvider getProviderOverride( - covariant FetchAccountInfoProvider provider, - ) { - return call( - accountMasterRecordKey: provider.accountMasterRecordKey, - ); - } - - static const Iterable? _dependencies = null; - - @override - Iterable? get dependencies => _dependencies; - - static const Iterable? _allTransitiveDependencies = null; - - @override - Iterable? get allTransitiveDependencies => - _allTransitiveDependencies; - - @override - String? get name => r'fetchAccountInfoProvider'; -} - -/// Get an account from the identity key and if it is logged in and we -/// have its secret available, return the account record contents -/// -/// Copied from [fetchAccountInfo]. -class FetchAccountInfoProvider extends AutoDisposeFutureProvider { - /// Get an account from the identity key and if it is logged in and we - /// have its secret available, return the account record contents - /// - /// Copied from [fetchAccountInfo]. - FetchAccountInfoProvider({ - required Typed accountMasterRecordKey, - }) : this._internal( - (ref) => fetchAccountInfo( - ref as FetchAccountInfoRef, - accountMasterRecordKey: accountMasterRecordKey, - ), - from: fetchAccountInfoProvider, - name: r'fetchAccountInfoProvider', - debugGetCreateSourceHash: - const bool.fromEnvironment('dart.vm.product') - ? null - : _$fetchAccountInfoHash, - dependencies: FetchAccountInfoFamily._dependencies, - allTransitiveDependencies: - FetchAccountInfoFamily._allTransitiveDependencies, - accountMasterRecordKey: accountMasterRecordKey, - ); - - FetchAccountInfoProvider._internal( - super._createNotifier, { - required super.name, - required super.dependencies, - required super.allTransitiveDependencies, - required super.debugGetCreateSourceHash, - required super.from, - required this.accountMasterRecordKey, - }) : super.internal(); - - final Typed accountMasterRecordKey; - - @override - Override overrideWith( - FutureOr Function(FetchAccountInfoRef provider) create, - ) { - return ProviderOverride( - origin: this, - override: FetchAccountInfoProvider._internal( - (ref) => create(ref as FetchAccountInfoRef), - from: from, - name: null, - dependencies: null, - allTransitiveDependencies: null, - debugGetCreateSourceHash: null, - accountMasterRecordKey: accountMasterRecordKey, - ), - ); - } - - @override - AutoDisposeFutureProviderElement createElement() { - return _FetchAccountInfoProviderElement(this); - } - - @override - bool operator ==(Object other) { - return other is FetchAccountInfoProvider && - other.accountMasterRecordKey == accountMasterRecordKey; - } - - @override - int get hashCode { - var hash = _SystemHash.combine(0, runtimeType.hashCode); - hash = _SystemHash.combine(hash, accountMasterRecordKey.hashCode); - - return _SystemHash.finish(hash); - } -} - -mixin FetchAccountInfoRef on AutoDisposeFutureProviderRef { - /// The parameter `accountMasterRecordKey` of this provider. - Typed get accountMasterRecordKey; -} - -class _FetchAccountInfoProviderElement - extends AutoDisposeFutureProviderElement - with FetchAccountInfoRef { - _FetchAccountInfoProviderElement(super.provider); - - @override - Typed get accountMasterRecordKey => - (origin as FetchAccountInfoProvider).accountMasterRecordKey; -} - -String _$fetchActiveAccountInfoHash() => - r'85276ff85b0e82c8d3c6313250954f5b578697d1'; - -/// Get the active account info -/// -/// Copied from [fetchActiveAccountInfo]. -@ProviderFor(fetchActiveAccountInfo) -final fetchActiveAccountInfoProvider = - AutoDisposeFutureProvider.internal( - fetchActiveAccountInfo, - name: r'fetchActiveAccountInfoProvider', - debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') - ? null - : _$fetchActiveAccountInfoHash, - dependencies: null, - allTransitiveDependencies: null, -); - -typedef FetchActiveAccountInfoRef - = AutoDisposeFutureProviderRef; -// ignore_for_file: type=lint -// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member diff --git a/lib/providers/chat.g.dart b/lib/providers/chat.g.dart deleted file mode 100644 index 3f2c8e8..0000000 --- a/lib/providers/chat.g.dart +++ /dev/null @@ -1,28 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'chat.dart'; - -// ************************************************************************** -// RiverpodGenerator -// ************************************************************************** - -String _$fetchChatListHash() => r'0c166082625799862128dff09d9286f64785ba6c'; - -/// Get the active account contact list -/// -/// Copied from [fetchChatList]. -@ProviderFor(fetchChatList) -final fetchChatListProvider = - AutoDisposeFutureProvider?>.internal( - fetchChatList, - name: r'fetchChatListProvider', - debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') - ? null - : _$fetchChatListHash, - dependencies: null, - allTransitiveDependencies: null, -); - -typedef FetchChatListRef = AutoDisposeFutureProviderRef?>; -// ignore_for_file: type=lint -// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member diff --git a/lib/providers/contact.g.dart b/lib/providers/contact.g.dart deleted file mode 100644 index b428110..0000000 --- a/lib/providers/contact.g.dart +++ /dev/null @@ -1,29 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'contact.dart'; - -// ************************************************************************** -// RiverpodGenerator -// ************************************************************************** - -String _$fetchContactListHash() => r'03e5b90435c331be87495d999a62a97af5b74d9e'; - -/// Get the active account contact list -/// -/// Copied from [fetchContactList]. -@ProviderFor(fetchContactList) -final fetchContactListProvider = - AutoDisposeFutureProvider?>.internal( - fetchContactList, - name: r'fetchContactListProvider', - debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') - ? null - : _$fetchContactListHash, - dependencies: null, - allTransitiveDependencies: null, -); - -typedef FetchContactListRef - = AutoDisposeFutureProviderRef?>; -// ignore_for_file: type=lint -// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member diff --git a/lib/providers/contact_invitation_list_manager.g.dart b/lib/providers/contact_invitation_list_manager.g.dart deleted file mode 100644 index ce2f160..0000000 --- a/lib/providers/contact_invitation_list_manager.g.dart +++ /dev/null @@ -1,202 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'contact_invitation_list_manager.dart'; - -// ************************************************************************** -// RiverpodGenerator -// ************************************************************************** - -String _$contactInvitationListManagerHash() => - r'8dda8e5005f0c0c921e3e8b7ce06e54bb5682085'; - -/// Copied from Dart SDK -class _SystemHash { - _SystemHash._(); - - static int combine(int hash, int value) { - // ignore: parameter_assignments - hash = 0x1fffffff & (hash + value); - // ignore: parameter_assignments - hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10)); - return hash ^ (hash >> 6); - } - - static int finish(int hash) { - // ignore: parameter_assignments - hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3)); - // ignore: parameter_assignments - hash = hash ^ (hash >> 11); - return 0x1fffffff & (hash + ((0x00003fff & hash) << 15)); - } -} - -abstract class _$ContactInvitationListManager - extends BuildlessAutoDisposeAsyncNotifier< - IList> { - late final ActiveAccountInfo activeAccountInfo; - - FutureOr> build( - ActiveAccountInfo activeAccountInfo, - ); -} - -////////////////////////////////////////////////// -////////////////////////////////////////////////// -/// -/// Copied from [ContactInvitationListManager]. -@ProviderFor(ContactInvitationListManager) -const contactInvitationListManagerProvider = - ContactInvitationListManagerFamily(); - -////////////////////////////////////////////////// -////////////////////////////////////////////////// -/// -/// Copied from [ContactInvitationListManager]. -class ContactInvitationListManagerFamily - extends Family>> { - ////////////////////////////////////////////////// -////////////////////////////////////////////////// - /// - /// Copied from [ContactInvitationListManager]. - const ContactInvitationListManagerFamily(); - - ////////////////////////////////////////////////// -////////////////////////////////////////////////// - /// - /// Copied from [ContactInvitationListManager]. - ContactInvitationListManagerProvider call( - ActiveAccountInfo activeAccountInfo, - ) { - return ContactInvitationListManagerProvider( - activeAccountInfo, - ); - } - - @override - ContactInvitationListManagerProvider getProviderOverride( - covariant ContactInvitationListManagerProvider provider, - ) { - return call( - provider.activeAccountInfo, - ); - } - - static const Iterable? _dependencies = null; - - @override - Iterable? get dependencies => _dependencies; - - static const Iterable? _allTransitiveDependencies = null; - - @override - Iterable? get allTransitiveDependencies => - _allTransitiveDependencies; - - @override - String? get name => r'contactInvitationListManagerProvider'; -} - -////////////////////////////////////////////////// -////////////////////////////////////////////////// -/// -/// Copied from [ContactInvitationListManager]. -class ContactInvitationListManagerProvider - extends AutoDisposeAsyncNotifierProviderImpl> { - ////////////////////////////////////////////////// -////////////////////////////////////////////////// - /// - /// Copied from [ContactInvitationListManager]. - ContactInvitationListManagerProvider( - ActiveAccountInfo activeAccountInfo, - ) : this._internal( - () => ContactInvitationListManager() - ..activeAccountInfo = activeAccountInfo, - from: contactInvitationListManagerProvider, - name: r'contactInvitationListManagerProvider', - debugGetCreateSourceHash: - const bool.fromEnvironment('dart.vm.product') - ? null - : _$contactInvitationListManagerHash, - dependencies: ContactInvitationListManagerFamily._dependencies, - allTransitiveDependencies: - ContactInvitationListManagerFamily._allTransitiveDependencies, - activeAccountInfo: activeAccountInfo, - ); - - ContactInvitationListManagerProvider._internal( - super._createNotifier, { - required super.name, - required super.dependencies, - required super.allTransitiveDependencies, - required super.debugGetCreateSourceHash, - required super.from, - required this.activeAccountInfo, - }) : super.internal(); - - final ActiveAccountInfo activeAccountInfo; - - @override - FutureOr> runNotifierBuild( - covariant ContactInvitationListManager notifier, - ) { - return notifier.build( - activeAccountInfo, - ); - } - - @override - Override overrideWith(ContactInvitationListManager Function() create) { - return ProviderOverride( - origin: this, - override: ContactInvitationListManagerProvider._internal( - () => create()..activeAccountInfo = activeAccountInfo, - from: from, - name: null, - dependencies: null, - allTransitiveDependencies: null, - debugGetCreateSourceHash: null, - activeAccountInfo: activeAccountInfo, - ), - ); - } - - @override - AutoDisposeAsyncNotifierProviderElement> createElement() { - return _ContactInvitationListManagerProviderElement(this); - } - - @override - bool operator ==(Object other) { - return other is ContactInvitationListManagerProvider && - other.activeAccountInfo == activeAccountInfo; - } - - @override - int get hashCode { - var hash = _SystemHash.combine(0, runtimeType.hashCode); - hash = _SystemHash.combine(hash, activeAccountInfo.hashCode); - - return _SystemHash.finish(hash); - } -} - -mixin ContactInvitationListManagerRef on AutoDisposeAsyncNotifierProviderRef< - IList> { - /// The parameter `activeAccountInfo` of this provider. - ActiveAccountInfo get activeAccountInfo; -} - -class _ContactInvitationListManagerProviderElement - extends AutoDisposeAsyncNotifierProviderElement< - ContactInvitationListManager, IList> - with ContactInvitationListManagerRef { - _ContactInvitationListManagerProviderElement(super.provider); - - @override - ActiveAccountInfo get activeAccountInfo => - (origin as ContactInvitationListManagerProvider).activeAccountInfo; -} -// ignore_for_file: type=lint -// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member diff --git a/lib/providers/contact_invite.g.dart b/lib/providers/contact_invite.g.dart deleted file mode 100644 index 758a54a..0000000 --- a/lib/providers/contact_invite.g.dart +++ /dev/null @@ -1,30 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'contact_invite.dart'; - -// ************************************************************************** -// RiverpodGenerator -// ************************************************************************** - -String _$fetchContactInvitationRecordsHash() => - r'ff0b2c68d42cb106602982b1fb56a7bd8183c04a'; - -/// 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?>; -// ignore_for_file: type=lint -// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member diff --git a/lib/providers/conversation.g.dart b/lib/providers/conversation.g.dart deleted file mode 100644 index a4875dd..0000000 --- a/lib/providers/conversation.g.dart +++ /dev/null @@ -1,28 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'conversation.dart'; - -// ************************************************************************** -// RiverpodGenerator -// ************************************************************************** - -String _$activeConversationMessagesHash() => - r'5579a9386f2046b156720ae799a0e77aca119b09'; - -/// See also [ActiveConversationMessages]. -@ProviderFor(ActiveConversationMessages) -final activeConversationMessagesProvider = AutoDisposeAsyncNotifierProvider< - ActiveConversationMessages, IList?>.internal( - ActiveConversationMessages.new, - name: r'activeConversationMessagesProvider', - debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') - ? null - : _$activeConversationMessagesHash, - dependencies: null, - allTransitiveDependencies: null, -); - -typedef _$ActiveConversationMessages - = AutoDisposeAsyncNotifier?>; -// ignore_for_file: type=lint -// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member diff --git a/lib/providers/local_accounts.dart b/lib/providers/local_accounts.dart deleted file mode 100644 index 4294adf..0000000 --- a/lib/providers/local_accounts.dart +++ /dev/null @@ -1,167 +0,0 @@ -import 'dart:async'; - -import 'package:fast_immutable_collections/fast_immutable_collections.dart'; -import 'package:riverpod_annotation/riverpod_annotation.dart'; - -import '../entities/entities.dart'; -import '../proto/proto.dart' as proto; -import '../tools/tools.dart'; -import '../veilid_init.dart'; -import '../veilid_support/veilid_support.dart'; -import 'logins.dart'; - -part 'local_accounts.g.dart'; - -const String veilidChatAccountKey = 'com.veilid.veilidchat'; - -// Local accounts table -@riverpod -class LocalAccounts extends _$LocalAccounts - with AsyncTableDBBacked> { - ////////////////////////////////////////////////////////////// - /// AsyncTableDBBacked - @override - String tableName() => 'local_account_manager'; - @override - String tableKeyName() => 'local_accounts'; - @override - IList valueFromJson(Object? obj) => obj != null - ? IList.fromJson( - obj, genericFromJson(LocalAccount.fromJson)) - : IList(); - @override - Object? valueToJson(IList val) => - val.toJson((la) => la.toJson()); - - /// Get all local account information - @override - FutureOr> build() async { - try { - await eventualVeilid.future; - return await load(); - } on Exception catch (e) { - log.error('Failed to load LocalAccounts table: $e', e); - return const IListConst([]); - } - } - - ////////////////////////////////////////////////////////////// - /// Mutators and Selectors - - /// Reorder accounts - Future reorderAccount(int oldIndex, int newIndex) async { - final localAccounts = state.requireValue; - final removedItem = Output(); - final updated = localAccounts - .removeAt(oldIndex, removedItem) - .insert(newIndex, removedItem.value!); - await store(updated); - state = AsyncValue.data(updated); - } - - /// Creates a new Account associated with master identity - /// Adds a logged-out LocalAccount to track its existence on this device - Future newLocalAccount( - {required IdentityMaster identityMaster, - required SecretKey identitySecret, - required String name, - required String pronouns, - EncryptionKeyType encryptionKeyType = EncryptionKeyType.none, - String encryptionKey = ''}) async { - final localAccounts = state.requireValue; - - // Add account with profile to DHT - await identityMaster.addAccountToIdentity( - identitySecret: identitySecret, - accountKey: veilidChatAccountKey, - createAccountCallback: (parent) async { - // Make empty contact list - final contactList = await (await DHTShortArray.create(parent: parent)) - .scope((r) async => r.record.ownedDHTRecordPointer); - - // Make empty contact invitation record list - final contactInvitationRecords = - await (await DHTShortArray.create(parent: parent)) - .scope((r) async => r.record.ownedDHTRecordPointer); - - // Make empty chat record list - final chatRecords = await (await DHTShortArray.create(parent: parent)) - .scope((r) async => r.record.ownedDHTRecordPointer); - - // Make account object - final account = proto.Account() - ..profile = (proto.Profile() - ..name = name - ..pronouns = pronouns) - ..contactList = contactList.toProto() - ..contactInvitationRecords = contactInvitationRecords.toProto() - ..chatList = chatRecords.toProto(); - return account; - }); - - // Encrypt identitySecret with key - final identitySecretBytes = await encryptSecretToBytes( - secret: identitySecret, - cryptoKind: identityMaster.identityRecordKey.kind, - encryptionKey: encryptionKey, - encryptionKeyType: encryptionKeyType); - - // Create local account object - // Does not contain the account key or its secret - // as that is not to be persisted, and only pulled from the identity key - // and optionally decrypted with the unlock password - final localAccount = LocalAccount( - identityMaster: identityMaster, - identitySecretBytes: identitySecretBytes, - encryptionKeyType: encryptionKeyType, - biometricsEnabled: false, - hiddenAccount: false, - name: name, - ); - - // Add local account object to internal store - final newLocalAccounts = localAccounts.add(localAccount); - await store(newLocalAccounts); - state = AsyncValue.data(newLocalAccounts); - - // Return local account object - return localAccount; - } - - /// Remove an account and wipe the messages for this account from this device - Future deleteLocalAccount(TypedKey accountMasterRecordKey) async { - final logins = ref.read(loginsProvider.notifier); - await logins.logout(accountMasterRecordKey); - - final localAccounts = state.requireValue; - final updated = localAccounts.removeWhere( - (la) => la.identityMaster.masterRecordKey == accountMasterRecordKey); - await store(updated); - state = AsyncValue.data(updated); - - // TO DO: wipe messages - - return true; - } - - /// Import an account from another VeilidChat instance - - /// Recover an account with the master identity secret - - /// Delete an account from all devices -} - -@riverpod -Future fetchLocalAccount(FetchLocalAccountRef ref, - {required TypedKey accountMasterRecordKey}) async { - final localAccounts = await ref.watch(localAccountsProvider.future); - try { - return localAccounts.firstWhere( - (e) => e.identityMaster.masterRecordKey == accountMasterRecordKey); - } on Exception catch (e) { - if (e is StateError) { - return null; - } - rethrow; - } -} diff --git a/lib/providers/local_accounts.g.dart b/lib/providers/local_accounts.g.dart deleted file mode 100644 index 026ddcc..0000000 --- a/lib/providers/local_accounts.g.dart +++ /dev/null @@ -1,179 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'local_accounts.dart'; - -// ************************************************************************** -// RiverpodGenerator -// ************************************************************************** - -String _$fetchLocalAccountHash() => r'e9f8ea0dd15031cc8145532e9cac73ab7f0f81be'; - -/// Copied from Dart SDK -class _SystemHash { - _SystemHash._(); - - static int combine(int hash, int value) { - // ignore: parameter_assignments - hash = 0x1fffffff & (hash + value); - // ignore: parameter_assignments - hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10)); - return hash ^ (hash >> 6); - } - - static int finish(int hash) { - // ignore: parameter_assignments - hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3)); - // ignore: parameter_assignments - hash = hash ^ (hash >> 11); - return 0x1fffffff & (hash + ((0x00003fff & hash) << 15)); - } -} - -/// See also [fetchLocalAccount]. -@ProviderFor(fetchLocalAccount) -const fetchLocalAccountProvider = FetchLocalAccountFamily(); - -/// See also [fetchLocalAccount]. -class FetchLocalAccountFamily extends Family> { - /// See also [fetchLocalAccount]. - const FetchLocalAccountFamily(); - - /// See also [fetchLocalAccount]. - FetchLocalAccountProvider call({ - required Typed accountMasterRecordKey, - }) { - return FetchLocalAccountProvider( - accountMasterRecordKey: accountMasterRecordKey, - ); - } - - @override - FetchLocalAccountProvider getProviderOverride( - covariant FetchLocalAccountProvider provider, - ) { - return call( - accountMasterRecordKey: provider.accountMasterRecordKey, - ); - } - - static const Iterable? _dependencies = null; - - @override - Iterable? get dependencies => _dependencies; - - static const Iterable? _allTransitiveDependencies = null; - - @override - Iterable? get allTransitiveDependencies => - _allTransitiveDependencies; - - @override - String? get name => r'fetchLocalAccountProvider'; -} - -/// See also [fetchLocalAccount]. -class FetchLocalAccountProvider - extends AutoDisposeFutureProvider { - /// See also [fetchLocalAccount]. - FetchLocalAccountProvider({ - required Typed accountMasterRecordKey, - }) : this._internal( - (ref) => fetchLocalAccount( - ref as FetchLocalAccountRef, - accountMasterRecordKey: accountMasterRecordKey, - ), - from: fetchLocalAccountProvider, - name: r'fetchLocalAccountProvider', - debugGetCreateSourceHash: - const bool.fromEnvironment('dart.vm.product') - ? null - : _$fetchLocalAccountHash, - dependencies: FetchLocalAccountFamily._dependencies, - allTransitiveDependencies: - FetchLocalAccountFamily._allTransitiveDependencies, - accountMasterRecordKey: accountMasterRecordKey, - ); - - FetchLocalAccountProvider._internal( - super._createNotifier, { - required super.name, - required super.dependencies, - required super.allTransitiveDependencies, - required super.debugGetCreateSourceHash, - required super.from, - required this.accountMasterRecordKey, - }) : super.internal(); - - final Typed accountMasterRecordKey; - - @override - Override overrideWith( - FutureOr Function(FetchLocalAccountRef provider) create, - ) { - return ProviderOverride( - origin: this, - override: FetchLocalAccountProvider._internal( - (ref) => create(ref as FetchLocalAccountRef), - from: from, - name: null, - dependencies: null, - allTransitiveDependencies: null, - debugGetCreateSourceHash: null, - accountMasterRecordKey: accountMasterRecordKey, - ), - ); - } - - @override - AutoDisposeFutureProviderElement createElement() { - return _FetchLocalAccountProviderElement(this); - } - - @override - bool operator ==(Object other) { - return other is FetchLocalAccountProvider && - other.accountMasterRecordKey == accountMasterRecordKey; - } - - @override - int get hashCode { - var hash = _SystemHash.combine(0, runtimeType.hashCode); - hash = _SystemHash.combine(hash, accountMasterRecordKey.hashCode); - - return _SystemHash.finish(hash); - } -} - -mixin FetchLocalAccountRef on AutoDisposeFutureProviderRef { - /// The parameter `accountMasterRecordKey` of this provider. - Typed get accountMasterRecordKey; -} - -class _FetchLocalAccountProviderElement - extends AutoDisposeFutureProviderElement - with FetchLocalAccountRef { - _FetchLocalAccountProviderElement(super.provider); - - @override - Typed get accountMasterRecordKey => - (origin as FetchLocalAccountProvider).accountMasterRecordKey; -} - -String _$localAccountsHash() => r'f19ec560b585d353219be82bc383b2c091660c53'; - -/// See also [LocalAccounts]. -@ProviderFor(LocalAccounts) -final localAccountsProvider = AutoDisposeAsyncNotifierProvider>.internal( - LocalAccounts.new, - name: r'localAccountsProvider', - debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') - ? null - : _$localAccountsHash, - dependencies: null, - allTransitiveDependencies: null, -); - -typedef _$LocalAccounts = AutoDisposeAsyncNotifier>; -// ignore_for_file: type=lint -// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member diff --git a/lib/providers/logins.dart b/lib/providers/logins.dart deleted file mode 100644 index 2617d04..0000000 --- a/lib/providers/logins.dart +++ /dev/null @@ -1,150 +0,0 @@ -import 'dart:async'; - -import 'package:fast_immutable_collections/fast_immutable_collections.dart'; -import 'package:riverpod_annotation/riverpod_annotation.dart'; - -import '../entities/entities.dart'; -import '../tools/tools.dart'; -import '../veilid_init.dart'; -import '../veilid_support/veilid_support.dart'; -import 'local_accounts.dart'; - -part 'logins.g.dart'; - -// Local account manager -@riverpod -class Logins extends _$Logins with AsyncTableDBBacked { - ////////////////////////////////////////////////////////////// - /// AsyncTableDBBacked - @override - String tableName() => 'local_account_manager'; - @override - String tableKeyName() => 'active_logins'; - @override - ActiveLogins valueFromJson(Object? obj) => obj != null - ? ActiveLogins.fromJson(obj as Map) - : ActiveLogins.empty(); - @override - Object? valueToJson(ActiveLogins val) => val.toJson(); - - /// Get all local account information - @override - FutureOr build() async { - try { - await eventualVeilid.future; - return await load(); - } on Exception catch (e) { - log.error('Failed to load ActiveLogins table: $e', e); - return const ActiveLogins(userLogins: IListConst([])); - } - } - - ////////////////////////////////////////////////////////////// - /// Mutators and Selectors - - Future switchToAccount(TypedKey? accountMasterRecordKey) async { - final current = state.requireValue; - if (accountMasterRecordKey != null) { - // Assert the specified record key can be found, will throw if not - final _ = current.userLogins.firstWhere( - (ul) => ul.accountMasterRecordKey == accountMasterRecordKey); - } - final updated = current.copyWith(activeUserLogin: accountMasterRecordKey); - await store(updated); - state = AsyncValue.data(updated); - } - - Future _decryptedLogin( - IdentityMaster identityMaster, SecretKey identitySecret) async { - final veilid = await eventualVeilid.future; - final cs = - await veilid.getCryptoSystem(identityMaster.identityRecordKey.kind); - final keyOk = await cs.validateKeyPair( - identityMaster.identityPublicKey, identitySecret); - if (!keyOk) { - throw Exception('Identity is corrupted'); - } - - // Read the identity key to get the account keys - final accountRecordInfo = await identityMaster.readAccountFromIdentity( - identitySecret: identitySecret, accountKey: veilidChatAccountKey); - - // Add to user logins and select it - final current = state.requireValue; - final now = veilid.now(); - final updated = current.copyWith( - userLogins: current.userLogins.replaceFirstWhere( - (ul) => ul.accountMasterRecordKey == identityMaster.masterRecordKey, - (ul) => ul != null - ? ul.copyWith(lastActive: now) - : UserLogin( - accountMasterRecordKey: identityMaster.masterRecordKey, - identitySecret: - TypedSecret(kind: cs.kind(), value: identitySecret), - accountRecordInfo: accountRecordInfo, - lastActive: now), - addIfNotFound: true), - activeUserLogin: identityMaster.masterRecordKey); - await store(updated); - state = AsyncValue.data(updated); - - return true; - } - - Future login(TypedKey accountMasterRecordKey, - EncryptionKeyType encryptionKeyType, String encryptionKey) async { - final localAccounts = ref.read(localAccountsProvider).requireValue; - - // Get account, throws if not found - final localAccount = localAccounts.firstWhere( - (la) => la.identityMaster.masterRecordKey == accountMasterRecordKey); - - // Log in with this local account - - // Derive key from password - if (localAccount.encryptionKeyType != encryptionKeyType) { - throw Exception('Wrong authentication type'); - } - - final identitySecret = await decryptSecretFromBytes( - secretBytes: localAccount.identitySecretBytes, - cryptoKind: localAccount.identityMaster.identityRecordKey.kind, - encryptionKeyType: localAccount.encryptionKeyType, - encryptionKey: encryptionKey, - ); - - // Validate this secret with the identity public key and log in - return _decryptedLogin(localAccount.identityMaster, identitySecret); - } - - Future logout(TypedKey? accountMasterRecordKey) async { - final current = state.requireValue; - final logoutUser = accountMasterRecordKey ?? current.activeUserLogin; - if (logoutUser == null) { - return; - } - final updated = current.copyWith( - activeUserLogin: current.activeUserLogin == logoutUser - ? null - : current.activeUserLogin, - userLogins: current.userLogins - .removeWhere((ul) => ul.accountMasterRecordKey == logoutUser)); - await store(updated); - state = AsyncValue.data(updated); - } -} - -@riverpod -Future fetchLogin(FetchLoginRef ref, - {required TypedKey accountMasterRecordKey}) async { - final activeLogins = await ref.watch(loginsProvider.future); - try { - return activeLogins.userLogins - .firstWhere((e) => e.accountMasterRecordKey == accountMasterRecordKey); - } on Exception catch (e) { - if (e is StateError) { - return null; - } - rethrow; - } -} diff --git a/lib/providers/logins.g.dart b/lib/providers/logins.g.dart deleted file mode 100644 index e4eee2e..0000000 --- a/lib/providers/logins.g.dart +++ /dev/null @@ -1,176 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'logins.dart'; - -// ************************************************************************** -// RiverpodGenerator -// ************************************************************************** - -String _$fetchLoginHash() => r'cfe13f5152f1275e6eccc698142abfd98170d9b9'; - -/// Copied from Dart SDK -class _SystemHash { - _SystemHash._(); - - static int combine(int hash, int value) { - // ignore: parameter_assignments - hash = 0x1fffffff & (hash + value); - // ignore: parameter_assignments - hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10)); - return hash ^ (hash >> 6); - } - - static int finish(int hash) { - // ignore: parameter_assignments - hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3)); - // ignore: parameter_assignments - hash = hash ^ (hash >> 11); - return 0x1fffffff & (hash + ((0x00003fff & hash) << 15)); - } -} - -/// See also [fetchLogin]. -@ProviderFor(fetchLogin) -const fetchLoginProvider = FetchLoginFamily(); - -/// See also [fetchLogin]. -class FetchLoginFamily extends Family> { - /// See also [fetchLogin]. - const FetchLoginFamily(); - - /// See also [fetchLogin]. - FetchLoginProvider call({ - required Typed accountMasterRecordKey, - }) { - return FetchLoginProvider( - accountMasterRecordKey: accountMasterRecordKey, - ); - } - - @override - FetchLoginProvider getProviderOverride( - covariant FetchLoginProvider provider, - ) { - return call( - accountMasterRecordKey: provider.accountMasterRecordKey, - ); - } - - static const Iterable? _dependencies = null; - - @override - Iterable? get dependencies => _dependencies; - - static const Iterable? _allTransitiveDependencies = null; - - @override - Iterable? get allTransitiveDependencies => - _allTransitiveDependencies; - - @override - String? get name => r'fetchLoginProvider'; -} - -/// See also [fetchLogin]. -class FetchLoginProvider extends AutoDisposeFutureProvider { - /// See also [fetchLogin]. - FetchLoginProvider({ - required Typed accountMasterRecordKey, - }) : this._internal( - (ref) => fetchLogin( - ref as FetchLoginRef, - accountMasterRecordKey: accountMasterRecordKey, - ), - from: fetchLoginProvider, - name: r'fetchLoginProvider', - debugGetCreateSourceHash: - const bool.fromEnvironment('dart.vm.product') - ? null - : _$fetchLoginHash, - dependencies: FetchLoginFamily._dependencies, - allTransitiveDependencies: - FetchLoginFamily._allTransitiveDependencies, - accountMasterRecordKey: accountMasterRecordKey, - ); - - FetchLoginProvider._internal( - super._createNotifier, { - required super.name, - required super.dependencies, - required super.allTransitiveDependencies, - required super.debugGetCreateSourceHash, - required super.from, - required this.accountMasterRecordKey, - }) : super.internal(); - - final Typed accountMasterRecordKey; - - @override - Override overrideWith( - FutureOr Function(FetchLoginRef provider) create, - ) { - return ProviderOverride( - origin: this, - override: FetchLoginProvider._internal( - (ref) => create(ref as FetchLoginRef), - from: from, - name: null, - dependencies: null, - allTransitiveDependencies: null, - debugGetCreateSourceHash: null, - accountMasterRecordKey: accountMasterRecordKey, - ), - ); - } - - @override - AutoDisposeFutureProviderElement createElement() { - return _FetchLoginProviderElement(this); - } - - @override - bool operator ==(Object other) { - return other is FetchLoginProvider && - other.accountMasterRecordKey == accountMasterRecordKey; - } - - @override - int get hashCode { - var hash = _SystemHash.combine(0, runtimeType.hashCode); - hash = _SystemHash.combine(hash, accountMasterRecordKey.hashCode); - - return _SystemHash.finish(hash); - } -} - -mixin FetchLoginRef on AutoDisposeFutureProviderRef { - /// The parameter `accountMasterRecordKey` of this provider. - Typed get accountMasterRecordKey; -} - -class _FetchLoginProviderElement - extends AutoDisposeFutureProviderElement with FetchLoginRef { - _FetchLoginProviderElement(super.provider); - - @override - Typed get accountMasterRecordKey => - (origin as FetchLoginProvider).accountMasterRecordKey; -} - -String _$loginsHash() => r'2660f71bb7903464187a93fba5c07e22041e8c40'; - -/// See also [Logins]. -@ProviderFor(Logins) -final loginsProvider = - AutoDisposeAsyncNotifierProvider.internal( - Logins.new, - name: r'loginsProvider', - debugGetCreateSourceHash: - const bool.fromEnvironment('dart.vm.product') ? null : _$loginsHash, - dependencies: null, - allTransitiveDependencies: null, -); - -typedef _$Logins = AutoDisposeAsyncNotifier; -// ignore_for_file: type=lint -// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member diff --git a/lib/providers/window_control.g.dart b/lib/providers/window_control.g.dart deleted file mode 100644 index d093cf4..0000000 --- a/lib/providers/window_control.g.dart +++ /dev/null @@ -1,26 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'window_control.dart'; - -// ************************************************************************** -// RiverpodGenerator -// ************************************************************************** - -String _$windowControlHash() => r'c6afcbe1d4bfcfc580c30393aac60624c5ceabe0'; - -/// See also [WindowControl]. -@ProviderFor(WindowControl) -final windowControlProvider = - AutoDisposeAsyncNotifierProvider.internal( - WindowControl.new, - name: r'windowControlProvider', - debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') - ? null - : _$windowControlHash, - dependencies: null, - allTransitiveDependencies: null, -); - -typedef _$WindowControl = AutoDisposeAsyncNotifier; -// ignore_for_file: type=lint -// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member diff --git a/lib/router/cubit/router_cubit.dart b/lib/router/cubit/router_cubit.dart new file mode 100644 index 0000000..70936e7 --- /dev/null +++ b/lib/router/cubit/router_cubit.dart @@ -0,0 +1,152 @@ +import 'dart:async'; + +import 'package:bloc/bloc.dart'; +import 'package:flutter/widgets.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:go_router/go_router.dart'; + +import '../../init.dart'; +import '../../local_account_manager/account_repository/account_repository.dart'; +import '../../old_to_refactor/pages/chat_only.dart'; +import '../../old_to_refactor/pages/developer.dart'; +import '../../old_to_refactor/pages/home.dart'; +import '../../old_to_refactor/pages/index.dart'; +import '../../old_to_refactor/pages/new_account.dart'; +import '../../old_to_refactor/pages/settings.dart'; +import '../../tools/tools.dart'; + +part 'router_cubit.freezed.dart'; +part 'router_cubit.g.dart'; +part 'router_state.dart'; + +class RouterCubit extends Cubit { + RouterCubit(AccountRepository accountRepository) + : super(const RouterState( + isInitialized: false, + hasAnyAccount: false, + hasActiveChat: false, + )) { + // Watch for changes that the router will care about + Future.delayed(Duration.zero, () async { + await eventualInitialized.future; + emit(state.copyWith(isInitialized: true)); + }); + // Subscribe to repository streams + _accountRepositorySubscription = + accountRepository.changes().listen((event) { + switch (event) { + case AccountRepositoryChange.localAccounts: + emit(state.copyWith( + hasAnyAccount: accountRepository.getLocalAccounts().isNotEmpty)); + break; + case AccountRepositoryChange.userLogins: + case AccountRepositoryChange.activeUserLogin: + break; + } + }); + _chatListRepositorySubscription = ... + } + + @override + Future close() async { + await _accountRepositorySubscription.cancel(); + await super.close(); + } + + /// Our application routes + List get routes => [ + GoRoute( + path: '/', + builder: (context, state) => const IndexPage(), + ), + GoRoute( + path: '/home', + builder: (context, state) => const HomePage(), + routes: [ + GoRoute( + path: 'settings', + builder: (context, state) => const SettingsPage(), + ), + GoRoute( + path: 'chat', + builder: (context, state) => const ChatOnlyPage(), + ), + ], + ), + GoRoute( + path: '/new_account', + builder: (context, state) => const NewAccountPage(), + routes: [ + GoRoute( + path: 'settings', + builder: (context, state) => const SettingsPage(), + ), + ], + ), + GoRoute( + path: '/developer', + builder: (context, state) => const DeveloperPage(), + ) + ]; + + /// Redirects when our state changes + String? redirect(BuildContext context, GoRouterState goRouterState) { + // if (state.isLoading || state.hasError) { + // return null; + // } + + // No matter where we are, if there's not + switch (goRouterState.matchedLocation) { + case '/': + + // Wait for veilid to be initialized + if (!eventualVeilid.isCompleted) { + return null; + } + + return state.hasAnyAccount ? '/home' : '/new_account'; + case '/new_account': + return state.hasAnyAccount ? '/home' : null; + case '/home': + if (!state.hasAnyAccount) { + return '/new_account'; + } + if (responsiveVisibility( + context: context, + tablet: false, + tabletLandscape: false, + desktop: false)) { + if (state.hasActiveChat) { + return '/home/chat'; + } + } + return null; + case '/home/chat': + if (!state.hasAnyAccount) { + return '/new_account'; + } + if (responsiveVisibility( + context: context, + tablet: false, + tabletLandscape: false, + desktop: false)) { + if (!state.hasActiveChat) { + return '/home'; + } + } else { + return '/home'; + } + return null; + case '/home/settings': + case '/new_account/settings': + return null; + case '/developer': + return null; + default: + return state.hasAnyAccount ? null : '/new_account'; + } + } + + late final StreamSubscription + _accountRepositorySubscription; +} diff --git a/lib/router/cubit/router_cubit.freezed.dart b/lib/router/cubit/router_cubit.freezed.dart new file mode 100644 index 0000000..a79e746 --- /dev/null +++ b/lib/router/cubit/router_cubit.freezed.dart @@ -0,0 +1,193 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'router_cubit.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +final _privateConstructorUsedError = UnsupportedError( + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#custom-getters-and-methods'); + +RouterState _$RouterStateFromJson(Map json) { + return _RouterState.fromJson(json); +} + +/// @nodoc +mixin _$RouterState { + bool get isInitialized => throw _privateConstructorUsedError; + bool get hasAnyAccount => throw _privateConstructorUsedError; + bool get hasActiveChat => throw _privateConstructorUsedError; + + Map toJson() => throw _privateConstructorUsedError; + @JsonKey(ignore: true) + $RouterStateCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $RouterStateCopyWith<$Res> { + factory $RouterStateCopyWith( + RouterState value, $Res Function(RouterState) then) = + _$RouterStateCopyWithImpl<$Res, RouterState>; + @useResult + $Res call({bool isInitialized, bool hasAnyAccount, bool hasActiveChat}); +} + +/// @nodoc +class _$RouterStateCopyWithImpl<$Res, $Val extends RouterState> + implements $RouterStateCopyWith<$Res> { + _$RouterStateCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? isInitialized = null, + Object? hasAnyAccount = null, + Object? hasActiveChat = null, + }) { + return _then(_value.copyWith( + isInitialized: null == isInitialized + ? _value.isInitialized + : isInitialized // ignore: cast_nullable_to_non_nullable + as bool, + hasAnyAccount: null == hasAnyAccount + ? _value.hasAnyAccount + : hasAnyAccount // ignore: cast_nullable_to_non_nullable + as bool, + hasActiveChat: null == hasActiveChat + ? _value.hasActiveChat + : hasActiveChat // ignore: cast_nullable_to_non_nullable + as bool, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$RouterStateImplCopyWith<$Res> + implements $RouterStateCopyWith<$Res> { + factory _$$RouterStateImplCopyWith( + _$RouterStateImpl value, $Res Function(_$RouterStateImpl) then) = + __$$RouterStateImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({bool isInitialized, bool hasAnyAccount, bool hasActiveChat}); +} + +/// @nodoc +class __$$RouterStateImplCopyWithImpl<$Res> + extends _$RouterStateCopyWithImpl<$Res, _$RouterStateImpl> + implements _$$RouterStateImplCopyWith<$Res> { + __$$RouterStateImplCopyWithImpl( + _$RouterStateImpl _value, $Res Function(_$RouterStateImpl) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? isInitialized = null, + Object? hasAnyAccount = null, + Object? hasActiveChat = null, + }) { + return _then(_$RouterStateImpl( + isInitialized: null == isInitialized + ? _value.isInitialized + : isInitialized // ignore: cast_nullable_to_non_nullable + as bool, + hasAnyAccount: null == hasAnyAccount + ? _value.hasAnyAccount + : hasAnyAccount // ignore: cast_nullable_to_non_nullable + as bool, + hasActiveChat: null == hasActiveChat + ? _value.hasActiveChat + : hasActiveChat // ignore: cast_nullable_to_non_nullable + as bool, + )); + } +} + +/// @nodoc +@JsonSerializable() +class _$RouterStateImpl implements _RouterState { + const _$RouterStateImpl( + {required this.isInitialized, + required this.hasAnyAccount, + required this.hasActiveChat}); + + factory _$RouterStateImpl.fromJson(Map json) => + _$$RouterStateImplFromJson(json); + + @override + final bool isInitialized; + @override + final bool hasAnyAccount; + @override + final bool hasActiveChat; + + @override + String toString() { + return 'RouterState(isInitialized: $isInitialized, hasAnyAccount: $hasAnyAccount, hasActiveChat: $hasActiveChat)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$RouterStateImpl && + (identical(other.isInitialized, isInitialized) || + other.isInitialized == isInitialized) && + (identical(other.hasAnyAccount, hasAnyAccount) || + other.hasAnyAccount == hasAnyAccount) && + (identical(other.hasActiveChat, hasActiveChat) || + other.hasActiveChat == hasActiveChat)); + } + + @JsonKey(ignore: true) + @override + int get hashCode => + Object.hash(runtimeType, isInitialized, hasAnyAccount, hasActiveChat); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$RouterStateImplCopyWith<_$RouterStateImpl> get copyWith => + __$$RouterStateImplCopyWithImpl<_$RouterStateImpl>(this, _$identity); + + @override + Map toJson() { + return _$$RouterStateImplToJson( + this, + ); + } +} + +abstract class _RouterState implements RouterState { + const factory _RouterState( + {required final bool isInitialized, + required final bool hasAnyAccount, + required final bool hasActiveChat}) = _$RouterStateImpl; + + factory _RouterState.fromJson(Map json) = + _$RouterStateImpl.fromJson; + + @override + bool get isInitialized; + @override + bool get hasAnyAccount; + @override + bool get hasActiveChat; + @override + @JsonKey(ignore: true) + _$$RouterStateImplCopyWith<_$RouterStateImpl> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/lib/router/cubit/router_cubit.g.dart b/lib/router/cubit/router_cubit.g.dart new file mode 100644 index 0000000..f67c770 --- /dev/null +++ b/lib/router/cubit/router_cubit.g.dart @@ -0,0 +1,21 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'router_cubit.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +_$RouterStateImpl _$$RouterStateImplFromJson(Map json) => + _$RouterStateImpl( + isInitialized: json['is_initialized'] as bool, + hasAnyAccount: json['has_any_account'] as bool, + hasActiveChat: json['has_active_chat'] as bool, + ); + +Map _$$RouterStateImplToJson(_$RouterStateImpl instance) => + { + 'is_initialized': instance.isInitialized, + 'has_any_account': instance.hasAnyAccount, + 'has_active_chat': instance.hasActiveChat, + }; diff --git a/lib/router/cubit/router_state.dart b/lib/router/cubit/router_state.dart new file mode 100644 index 0000000..072f797 --- /dev/null +++ b/lib/router/cubit/router_state.dart @@ -0,0 +1,12 @@ +part of 'router_cubit.dart'; + +@freezed +class RouterState with _$RouterState { + const factory RouterState( + {required bool isInitialized, + required bool hasAnyAccount, + required bool hasActiveChat}) = _RouterState; + + factory RouterState.fromJson(dynamic json) => + _$RouterStateFromJson(json as Map); +} diff --git a/lib/router/make_router.dart b/lib/router/make_router.dart new file mode 100644 index 0000000..37e7a7b --- /dev/null +++ b/lib/router/make_router.dart @@ -0,0 +1,20 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'package:stream_transform/stream_transform.dart'; + +import '../tools/stream_listenable.dart'; +import 'cubit/router_cubit.dart'; + +final _key = GlobalKey(debugLabel: 'routerKey'); + +/// This simple provider caches our GoRouter. +GoRouter router({required RouterCubit routerCubit}) => GoRouter( + navigatorKey: _key, + refreshListenable: StreamListenable( + routerCubit.stream.startWith(routerCubit.state).distinct()), + debugLogDiagnostics: kDebugMode, + initialLocation: '/', + routes: routerCubit.routes, + redirect: routerCubit.redirect, + ); diff --git a/lib/router/router.dart b/lib/router/router.dart index d0f2cf5..5090b83 100644 --- a/lib/router/router.dart +++ b/lib/router/router.dart @@ -1,23 +1,2 @@ -import 'package:flutter/material.dart'; -import 'package:go_router/go_router.dart'; -import 'package:riverpod_annotation/riverpod_annotation.dart'; - -import 'router_notifier.dart'; - -part 'router.g.dart'; - -final _key = GlobalKey(debugLabel: 'routerKey'); - -/// This simple provider caches our GoRouter. -@riverpod -GoRouter router(RouterRef ref) { - final notifier = ref.watch(routerNotifierProvider.notifier); - return GoRouter( - navigatorKey: _key, - refreshListenable: notifier, - debugLogDiagnostics: true, - initialLocation: '/', - routes: notifier.routes, - redirect: notifier.redirect, - ); -} +export 'cubit/router_cubit.dart'; +export 'make_router.dart'; diff --git a/lib/router/router.g.dart b/lib/router/router.g.dart deleted file mode 100644 index b015d4b..0000000 --- a/lib/router/router.g.dart +++ /dev/null @@ -1,26 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'router.dart'; - -// ************************************************************************** -// RiverpodGenerator -// ************************************************************************** - -String _$routerHash() => r'86eecb1955be62ef8e6f6efcec0fa615289cb823'; - -/// This simple provider caches our GoRouter. -/// -/// Copied from [router]. -@ProviderFor(router) -final routerProvider = AutoDisposeProvider.internal( - router, - name: r'routerProvider', - debugGetCreateSourceHash: - const bool.fromEnvironment('dart.vm.product') ? null : _$routerHash, - dependencies: null, - allTransitiveDependencies: null, -); - -typedef RouterRef = AutoDisposeProviderRef; -// ignore_for_file: type=lint -// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member diff --git a/lib/router/router_notifier.dart b/lib/router/router_notifier.dart deleted file mode 100644 index a4f7bb6..0000000 --- a/lib/router/router_notifier.dart +++ /dev/null @@ -1,160 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:go_router/go_router.dart'; -import 'package:riverpod_annotation/riverpod_annotation.dart'; - -import '../pages/chat_only.dart'; -import '../pages/developer.dart'; -import '../pages/home.dart'; -import '../pages/index.dart'; -import '../pages/new_account.dart'; -import '../pages/settings.dart'; -import '../providers/chat.dart'; -import '../providers/local_accounts.dart'; -import '../tools/responsive.dart'; -import '../veilid_init.dart'; - -part 'router_notifier.g.dart'; - -@riverpod -class RouterNotifier extends _$RouterNotifier implements Listenable { - /// GoRouter listener - VoidCallback? routerListener; - - /// Do we need to make or import an account immediately? - bool hasAnyAccount = false; - bool hasActiveChat = false; - - /// AsyncNotifier build - @override - Future build() async { - hasAnyAccount = await ref.watch( - localAccountsProvider.selectAsync((data) => data.isNotEmpty), - ); - hasActiveChat = ref.watch(activeChatStateProvider) != null; - - // When this notifier's state changes, inform GoRouter - ref.listenSelf((_, __) { - if (state.isLoading) { - return; - } - routerListener?.call(); - }); - } - - /// Redirects when our state changes - String? redirect(BuildContext context, GoRouterState state) { - if (this.state.isLoading || this.state.hasError) { - return null; - } - - // No matter where we are, if there's not - switch (state.matchedLocation) { - case '/': - - // Wait for veilid to be initialized - if (!eventualVeilid.isCompleted) { - return null; - } - - return hasAnyAccount ? '/home' : '/new_account'; - case '/new_account': - return hasAnyAccount ? '/home' : null; - case '/home': - if (!hasAnyAccount) { - return '/new_account'; - } - if (responsiveVisibility( - context: context, - tablet: false, - tabletLandscape: false, - desktop: false)) { - if (hasActiveChat) { - return '/home/chat'; - } - } - return null; - case '/home/chat': - if (!hasAnyAccount) { - return '/new_account'; - } - if (responsiveVisibility( - context: context, - tablet: false, - tabletLandscape: false, - desktop: false)) { - if (!hasActiveChat) { - return '/home'; - } - } else { - return '/home'; - } - return null; - case '/home/settings': - case '/new_account/settings': - return null; - case '/developer': - return null; - default: - return hasAnyAccount ? null : '/new_account'; - } - } - - /// Our application routes - List get routes => [ - GoRoute( - path: '/', - builder: (context, state) => const IndexPage(), - ), - GoRoute( - path: '/home', - builder: (context, state) => const HomePage(), - routes: [ - GoRoute( - path: 'settings', - builder: (context, state) => const SettingsPage(), - ), - GoRoute( - path: 'chat', - builder: (context, state) => const ChatOnlyPage(), - ), - ], - ), - GoRoute( - path: '/new_account', - builder: (context, state) => const NewAccountPage(), - routes: [ - GoRoute( - path: 'settings', - builder: (context, state) => const SettingsPage(), - ), - ], - ), - GoRoute( - path: '/developer', - builder: (context, state) => const DeveloperPage(), - ) - ]; - - /////////////////////////////////////////////////////////////////////////// - /// Listenable - - /// Adds [GoRouter]'s listener as specified by its [Listenable]. - /// [GoRouteInformationProvider] uses this method on creation to handle its - /// internal [ChangeNotifier]. - /// Check out the internal implementation of [GoRouter] and - /// [GoRouteInformationProvider] to see this in action. - @override - void addListener(VoidCallback listener) { - routerListener = listener; - } - - /// Removes [GoRouter]'s listener as specified by its [Listenable]. - /// [GoRouteInformationProvider] uses this method when disposing, - /// so that it removes its callback when destroyed. - /// Check out the internal implementation of [GoRouter] and - /// [GoRouteInformationProvider] to see this in action. - @override - void removeListener(VoidCallback listener) { - routerListener = null; - } -} diff --git a/lib/router/router_notifier.g.dart b/lib/router/router_notifier.g.dart deleted file mode 100644 index fe12322..0000000 --- a/lib/router/router_notifier.g.dart +++ /dev/null @@ -1,26 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'router_notifier.dart'; - -// ************************************************************************** -// RiverpodGenerator -// ************************************************************************** - -String _$routerNotifierHash() => r'6f52ed95f090f2d198d358e7526a91511c0a61e5'; - -/// See also [RouterNotifier]. -@ProviderFor(RouterNotifier) -final routerNotifierProvider = - AutoDisposeAsyncNotifierProvider.internal( - RouterNotifier.new, - name: r'routerNotifierProvider', - debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') - ? null - : _$routerNotifierHash, - dependencies: null, - allTransitiveDependencies: null, -); - -typedef _$RouterNotifier = AutoDisposeAsyncNotifier; -// ignore_for_file: type=lint -// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member diff --git a/lib/tools/radix_generator.dart b/lib/theme/radix_generator.dart similarity index 99% rename from lib/tools/radix_generator.dart rename to lib/theme/radix_generator.dart index b805374..609f923 100644 --- a/lib/tools/radix_generator.dart +++ b/lib/theme/radix_generator.dart @@ -2,7 +2,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_chat_ui/flutter_chat_ui.dart'; import 'package:radix_colors/radix_colors.dart'; -import 'theme_service.dart'; +import 'scale_color.dart'; +import 'scale_scheme.dart'; enum RadixThemeColor { scarlet, // tomato + red + violet diff --git a/lib/theme/scale_color.dart b/lib/theme/scale_color.dart new file mode 100644 index 0000000..1b2f112 --- /dev/null +++ b/lib/theme/scale_color.dart @@ -0,0 +1,91 @@ +import 'dart:ui'; + +class ScaleColor { + ScaleColor({ + required this.appBackground, + required this.subtleBackground, + required this.elementBackground, + required this.hoverElementBackground, + required this.activeElementBackground, + required this.subtleBorder, + required this.border, + required this.hoverBorder, + required this.background, + required this.hoverBackground, + required this.subtleText, + required this.text, + }); + + Color appBackground; + Color subtleBackground; + Color elementBackground; + Color hoverElementBackground; + Color activeElementBackground; + Color subtleBorder; + Color border; + Color hoverBorder; + Color background; + Color hoverBackground; + Color subtleText; + Color text; + + ScaleColor copyWith( + {Color? appBackground, + Color? subtleBackground, + Color? elementBackground, + Color? hoverElementBackground, + Color? activeElementBackground, + 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, + activeElementBackground: + activeElementBackground ?? this.activeElementBackground, + 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), + activeElementBackground: Color.lerp( + a.activeElementBackground, b.activeElementBackground, 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), + ); +} diff --git a/lib/theme/scale_scheme.dart b/lib/theme/scale_scheme.dart new file mode 100644 index 0000000..74e51bc --- /dev/null +++ b/lib/theme/scale_scheme.dart @@ -0,0 +1,53 @@ +import 'package:flutter/material.dart'; + +import 'scale_color.dart'; + +class ScaleScheme extends ThemeExtension { + ScaleScheme( + {required this.primaryScale, + required this.primaryAlphaScale, + required this.secondaryScale, + required this.tertiaryScale, + required this.grayScale, + required this.errorScale}); + + final ScaleColor primaryScale; + final ScaleColor primaryAlphaScale; + final ScaleColor secondaryScale; + final ScaleColor tertiaryScale; + final ScaleColor grayScale; + final ScaleColor errorScale; + + @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), + ); + } +} diff --git a/lib/theme/theme.dart b/lib/theme/theme.dart new file mode 100644 index 0000000..064b1bf --- /dev/null +++ b/lib/theme/theme.dart @@ -0,0 +1,3 @@ +export 'scale_scheme.dart'; +export 'theme_preference.dart'; +export 'theme_repository.dart'; diff --git a/lib/theme/theme_preference.dart b/lib/theme/theme_preference.dart new file mode 100644 index 0000000..0ac25e5 --- /dev/null +++ b/lib/theme/theme_preference.dart @@ -0,0 +1,52 @@ +import 'package:change_case/change_case.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'theme_preference.freezed.dart'; +part 'theme_preference.g.dart'; + +// Theme supports light and dark mode, optionally selected by the +// operating system +enum BrightnessPreference { + system, + light, + dark; + + factory BrightnessPreference.fromJson(dynamic j) => + BrightnessPreference.values.byName((j as String).toCamelCase()); + + String toJson() => name.toPascalCase(); +} + +// Theme supports multiple color variants based on 'Radix' +enum ColorPreference { + // Radix Colors + scarlet, + babydoll, + vapor, + gold, + garden, + forest, + arctic, + lapis, + eggplant, + lime, + grim, + // Accessible Colors + contrast; + + factory ColorPreference.fromJson(dynamic j) => + ColorPreference.values.byName((j as String).toCamelCase()); + String toJson() => name.toPascalCase(); +} + +@freezed +class ThemePreferences with _$ThemePreferences { + const factory ThemePreferences({ + required BrightnessPreference brightnessPreference, + required ColorPreference colorPreference, + required double displayScale, + }) = _ThemePreferences; + + factory ThemePreferences.fromJson(dynamic json) => + _$ThemePreferencesFromJson(json as Map); +} diff --git a/lib/theme/theme_preference.freezed.dart b/lib/theme/theme_preference.freezed.dart new file mode 100644 index 0000000..3b2b9a4 --- /dev/null +++ b/lib/theme/theme_preference.freezed.dart @@ -0,0 +1,201 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'theme_preference.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +final _privateConstructorUsedError = UnsupportedError( + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#custom-getters-and-methods'); + +ThemePreferences _$ThemePreferencesFromJson(Map json) { + return _ThemePreferences.fromJson(json); +} + +/// @nodoc +mixin _$ThemePreferences { + BrightnessPreference get brightnessPreference => + throw _privateConstructorUsedError; + ColorPreference get colorPreference => throw _privateConstructorUsedError; + double get displayScale => throw _privateConstructorUsedError; + + Map toJson() => throw _privateConstructorUsedError; + @JsonKey(ignore: true) + $ThemePreferencesCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $ThemePreferencesCopyWith<$Res> { + factory $ThemePreferencesCopyWith( + ThemePreferences value, $Res Function(ThemePreferences) then) = + _$ThemePreferencesCopyWithImpl<$Res, ThemePreferences>; + @useResult + $Res call( + {BrightnessPreference brightnessPreference, + ColorPreference colorPreference, + double displayScale}); +} + +/// @nodoc +class _$ThemePreferencesCopyWithImpl<$Res, $Val extends ThemePreferences> + implements $ThemePreferencesCopyWith<$Res> { + _$ThemePreferencesCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? brightnessPreference = null, + Object? colorPreference = null, + Object? displayScale = null, + }) { + return _then(_value.copyWith( + brightnessPreference: null == brightnessPreference + ? _value.brightnessPreference + : brightnessPreference // ignore: cast_nullable_to_non_nullable + as BrightnessPreference, + colorPreference: null == colorPreference + ? _value.colorPreference + : colorPreference // ignore: cast_nullable_to_non_nullable + as ColorPreference, + displayScale: null == displayScale + ? _value.displayScale + : displayScale // ignore: cast_nullable_to_non_nullable + as double, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$ThemePreferencesImplCopyWith<$Res> + implements $ThemePreferencesCopyWith<$Res> { + factory _$$ThemePreferencesImplCopyWith(_$ThemePreferencesImpl value, + $Res Function(_$ThemePreferencesImpl) then) = + __$$ThemePreferencesImplCopyWithImpl<$Res>; + @override + @useResult + $Res call( + {BrightnessPreference brightnessPreference, + ColorPreference colorPreference, + double displayScale}); +} + +/// @nodoc +class __$$ThemePreferencesImplCopyWithImpl<$Res> + extends _$ThemePreferencesCopyWithImpl<$Res, _$ThemePreferencesImpl> + implements _$$ThemePreferencesImplCopyWith<$Res> { + __$$ThemePreferencesImplCopyWithImpl(_$ThemePreferencesImpl _value, + $Res Function(_$ThemePreferencesImpl) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? brightnessPreference = null, + Object? colorPreference = null, + Object? displayScale = null, + }) { + return _then(_$ThemePreferencesImpl( + brightnessPreference: null == brightnessPreference + ? _value.brightnessPreference + : brightnessPreference // ignore: cast_nullable_to_non_nullable + as BrightnessPreference, + colorPreference: null == colorPreference + ? _value.colorPreference + : colorPreference // ignore: cast_nullable_to_non_nullable + as ColorPreference, + displayScale: null == displayScale + ? _value.displayScale + : displayScale // ignore: cast_nullable_to_non_nullable + as double, + )); + } +} + +/// @nodoc +@JsonSerializable() +class _$ThemePreferencesImpl implements _ThemePreferences { + const _$ThemePreferencesImpl( + {required this.brightnessPreference, + required this.colorPreference, + required this.displayScale}); + + factory _$ThemePreferencesImpl.fromJson(Map json) => + _$$ThemePreferencesImplFromJson(json); + + @override + final BrightnessPreference brightnessPreference; + @override + final ColorPreference colorPreference; + @override + final double displayScale; + + @override + String toString() { + return 'ThemePreferences(brightnessPreference: $brightnessPreference, colorPreference: $colorPreference, displayScale: $displayScale)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$ThemePreferencesImpl && + (identical(other.brightnessPreference, brightnessPreference) || + other.brightnessPreference == brightnessPreference) && + (identical(other.colorPreference, colorPreference) || + other.colorPreference == colorPreference) && + (identical(other.displayScale, displayScale) || + other.displayScale == displayScale)); + } + + @JsonKey(ignore: true) + @override + int get hashCode => Object.hash( + runtimeType, brightnessPreference, colorPreference, displayScale); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$ThemePreferencesImplCopyWith<_$ThemePreferencesImpl> get copyWith => + __$$ThemePreferencesImplCopyWithImpl<_$ThemePreferencesImpl>( + this, _$identity); + + @override + Map toJson() { + return _$$ThemePreferencesImplToJson( + this, + ); + } +} + +abstract class _ThemePreferences implements ThemePreferences { + const factory _ThemePreferences( + {required final BrightnessPreference brightnessPreference, + required final ColorPreference colorPreference, + required final double displayScale}) = _$ThemePreferencesImpl; + + factory _ThemePreferences.fromJson(Map json) = + _$ThemePreferencesImpl.fromJson; + + @override + BrightnessPreference get brightnessPreference; + @override + ColorPreference get colorPreference; + @override + double get displayScale; + @override + @JsonKey(ignore: true) + _$$ThemePreferencesImplCopyWith<_$ThemePreferencesImpl> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/lib/theme/theme_preference.g.dart b/lib/theme/theme_preference.g.dart new file mode 100644 index 0000000..6f33c43 --- /dev/null +++ b/lib/theme/theme_preference.g.dart @@ -0,0 +1,24 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'theme_preference.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +_$ThemePreferencesImpl _$$ThemePreferencesImplFromJson( + Map json) => + _$ThemePreferencesImpl( + brightnessPreference: + BrightnessPreference.fromJson(json['brightness_preference']), + colorPreference: ColorPreference.fromJson(json['color_preference']), + displayScale: (json['display_scale'] as num).toDouble(), + ); + +Map _$$ThemePreferencesImplToJson( + _$ThemePreferencesImpl instance) => + { + 'brightness_preference': instance.brightnessPreference.toJson(), + 'color_preference': instance.colorPreference.toJson(), + 'display_scale': instance.displayScale, + }; diff --git a/lib/theme/theme_repository.dart b/lib/theme/theme_repository.dart new file mode 100644 index 0000000..c717928 --- /dev/null +++ b/lib/theme/theme_repository.dart @@ -0,0 +1,132 @@ +// ignore_for_file: always_put_required_named_parameters_first + +import 'dart:convert'; + +import 'package:flutter/material.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +import 'radix_generator.dart'; +import 'theme_preference.dart'; + +//////////////////////////////////////////////////////////////////////// + +class ThemeRepository { + ThemeRepository._({required SharedPreferences sharedPreferences}) + : _sharedPreferences = sharedPreferences, + _themePreferences = defaultThemePreferences; + + final SharedPreferences _sharedPreferences; + ThemePreferences _themePreferences; + ThemeData? _cachedThemeData; + + /// Singleton instance of ThemeRepository + static ThemeRepository? _instance; + static Future get instance async { + if (_instance == null) { + final sharedPreferences = await SharedPreferences.getInstance(); + final instance = ThemeRepository._(sharedPreferences: sharedPreferences); + await instance.load(); + _instance = instance; + } + return _instance!; + } + + static bool get isPlatformDark => + WidgetsBinding.instance.platformDispatcher.platformBrightness == + Brightness.dark; + + /// Defaults + static ThemePreferences get defaultThemePreferences => const ThemePreferences( + colorPreference: ColorPreference.vapor, + brightnessPreference: BrightnessPreference.system, + displayScale: 1, + ); + + /// Get theme preferences + ThemePreferences get themePreferences => _themePreferences; + + /// Set theme preferences + void setThemePreferences(ThemePreferences themePreferences) { + _themePreferences = themePreferences; + _cachedThemeData = null; + } + + /// Load theme preferences from storage + Future load() async { + final themePreferencesJson = + _sharedPreferences.getString('themePreferences'); + + ThemePreferences? newThemePreferences; + if (themePreferencesJson != null) { + try { + newThemePreferences = + ThemePreferences.fromJson(jsonDecode(themePreferencesJson)); + // ignore: avoid_catches_without_on_clauses + } catch (_) { + // ignore + } + } + setThemePreferences(newThemePreferences ?? defaultThemePreferences); + } + + /// Save theme preferences to storage + Future save() async { + await _sharedPreferences.setString( + 'themePreferences', jsonEncode(_themePreferences.toJson())); + } + + /// Get material 'ThemeData' for existinb + ThemeData themeData() { + final cachedThemeData = _cachedThemeData; + if (cachedThemeData != null) { + return cachedThemeData; + } + late final Brightness brightness; + switch (_themePreferences.brightnessPreference) { + case BrightnessPreference.system: + if (isPlatformDark) { + brightness = Brightness.dark; + } else { + brightness = Brightness.light; + } + case BrightnessPreference.light: + brightness = Brightness.light; + case BrightnessPreference.dark: + brightness = Brightness.dark; + } + + late final ThemeData themeData; + switch (_themePreferences.colorPreference) { + // Special cases + case ColorPreference.contrast: + // xxx do contrastGenerator + themeData = radixGenerator(brightness, RadixThemeColor.grim); + // Generate from Radix + case ColorPreference.scarlet: + themeData = radixGenerator(brightness, RadixThemeColor.scarlet); + case ColorPreference.babydoll: + themeData = radixGenerator(brightness, RadixThemeColor.babydoll); + case ColorPreference.vapor: + themeData = radixGenerator(brightness, RadixThemeColor.vapor); + case ColorPreference.gold: + themeData = radixGenerator(brightness, RadixThemeColor.gold); + case ColorPreference.garden: + themeData = radixGenerator(brightness, RadixThemeColor.garden); + case ColorPreference.forest: + themeData = radixGenerator(brightness, RadixThemeColor.forest); + case ColorPreference.arctic: + themeData = radixGenerator(brightness, RadixThemeColor.arctic); + case ColorPreference.lapis: + themeData = radixGenerator(brightness, RadixThemeColor.lapis); + case ColorPreference.eggplant: + themeData = radixGenerator(brightness, RadixThemeColor.eggplant); + case ColorPreference.lime: + themeData = radixGenerator(brightness, RadixThemeColor.lime); + case ColorPreference.grim: + themeData = radixGenerator(brightness, RadixThemeColor.grim); + } + + _cachedThemeData = themeData; + return themeData; + } +} diff --git a/lib/theme/theme_service.dart b/lib/theme/theme_service.dart new file mode 100644 index 0000000..5464c04 --- /dev/null +++ b/lib/theme/theme_service.dart @@ -0,0 +1,108 @@ +// ignore_for_file: always_put_required_named_parameters_first + +import 'dart:convert'; + +import 'package:flutter/material.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +import '../entities/preferences.dart'; +import 'radix_generator.dart'; + +//////////////////////////////////////////////////////////////////////// + +class ThemeService { + ThemeService._(); + static late SharedPreferences prefs; + static ThemeService? _instance; + + static Future get instance async { + if (_instance == null) { + prefs = await SharedPreferences.getInstance(); + _instance = ThemeService._(); + } + return _instance!; + } + + static bool get isPlatformDark => + WidgetsBinding.instance.platformDispatcher.platformBrightness == + Brightness.dark; + + ThemeData get initial { + final themePreferences = load(); + return get(themePreferences); + } + + ThemePreferences load() { + final themePreferencesJson = prefs.getString('themePreferences'); + ThemePreferences? themePreferences; + if (themePreferencesJson != null) { + try { + themePreferences = + ThemePreferences.fromJson(jsonDecode(themePreferencesJson)); + // ignore: avoid_catches_without_on_clauses + } catch (_) { + // ignore + } + } + return themePreferences ?? + const ThemePreferences( + colorPreference: ColorPreference.vapor, + brightnessPreference: BrightnessPreference.system, + displayScale: 1, + ); + } + + Future save(ThemePreferences themePreferences) async { + await prefs.setString( + 'themePreferences', jsonEncode(themePreferences.toJson())); + } + + ThemeData get(ThemePreferences themePreferences) { + late final Brightness brightness; + switch (themePreferences.brightnessPreference) { + case BrightnessPreference.system: + if (isPlatformDark) { + brightness = Brightness.dark; + } else { + brightness = Brightness.light; + } + case BrightnessPreference.light: + brightness = Brightness.light; + case BrightnessPreference.dark: + brightness = Brightness.dark; + } + + late final ThemeData themeData; + switch (themePreferences.colorPreference) { + // Special cases + case ColorPreference.contrast: + // xxx do contrastGenerator + themeData = radixGenerator(brightness, RadixThemeColor.grim); + // Generate from Radix + case ColorPreference.scarlet: + themeData = radixGenerator(brightness, RadixThemeColor.scarlet); + case ColorPreference.babydoll: + themeData = radixGenerator(brightness, RadixThemeColor.babydoll); + case ColorPreference.vapor: + themeData = radixGenerator(brightness, RadixThemeColor.vapor); + case ColorPreference.gold: + themeData = radixGenerator(brightness, RadixThemeColor.gold); + case ColorPreference.garden: + themeData = radixGenerator(brightness, RadixThemeColor.garden); + case ColorPreference.forest: + themeData = radixGenerator(brightness, RadixThemeColor.forest); + case ColorPreference.arctic: + themeData = radixGenerator(brightness, RadixThemeColor.arctic); + case ColorPreference.lapis: + themeData = radixGenerator(brightness, RadixThemeColor.lapis); + case ColorPreference.eggplant: + themeData = radixGenerator(brightness, RadixThemeColor.eggplant); + case ColorPreference.lime: + themeData = radixGenerator(brightness, RadixThemeColor.lime); + case ColorPreference.grim: + themeData = radixGenerator(brightness, RadixThemeColor.grim); + } + + return themeData; + } +} diff --git a/lib/tick.dart b/lib/tick.dart index 7e55da5..55eecbf 100644 --- a/lib/tick.dart +++ b/lib/tick.dart @@ -4,21 +4,20 @@ import 'dart:async'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'proto/proto.dart' as proto; -import 'providers/account.dart'; -import 'providers/chat.dart'; -import 'providers/connection_state.dart'; -import 'providers/contact.dart'; -import 'providers/contact_invite.dart'; -import 'providers/conversation.dart'; +import 'old_to_refactor/providers/account.dart'; +import 'old_to_refactor/providers/chat.dart'; +import 'old_to_refactor/providers/connection_state.dart'; +import 'old_to_refactor/providers/contact.dart'; +import 'old_to_refactor/providers/contact_invite.dart'; +import 'old_to_refactor/providers/conversation.dart'; import 'veilid_init.dart'; const int ticksPerContactInvitationCheck = 5; const int ticksPerNewMessageCheck = 5; -class BackgroundTicker extends ConsumerStatefulWidget { +class BackgroundTicker extends StatefulWidget { const BackgroundTicker({required this.builder, super.key}); final Widget Function(BuildContext) builder; @@ -33,7 +32,7 @@ class BackgroundTicker extends ConsumerStatefulWidget { } } -class BackgroundTickerState extends ConsumerState { +class BackgroundTickerState extends State { Timer? _tickTimer; bool _inTick = false; int _contactInvitationCheckTick = 0; diff --git a/lib/tools/async_table_db_backed_cubit.dart b/lib/tools/async_table_db_backed_cubit.dart new file mode 100644 index 0000000..a4ca440 --- /dev/null +++ b/lib/tools/async_table_db_backed_cubit.dart @@ -0,0 +1,51 @@ +import 'dart:async'; + +import 'package:bloc/bloc.dart'; + +import '../../tools/tools.dart'; +import '../../veilid_init.dart'; +import '../../veilid_support/veilid_support.dart'; + +abstract class AsyncTableDBBackedCubit extends Cubit> + with TableDBBacked { + AsyncTableDBBackedCubit() : super(const AsyncValue.loading()) { + unawaited(Future.delayed(Duration.zero, _build)); + } + + Future _build() async { + try { + await eventualVeilid.future; + emit(AsyncValue.data(await load())); + } on Exception catch (e, stackTrace) { + emit(AsyncValue.error(e, stackTrace)); + } + } + + Future readyData() async { + final stateStream = stream.distinct(); + await for (final AsyncValue av in stateStream) { + final d = av.when( + data: (value) => value, loading: () => null, error: (e, s) => null); + if (d != null) { + return d; + } + final ef = av.when( + data: (value) => null, + loading: () => null, + error: Future.error); + if (ef != null) { + return ef; + } + } + return Future.error( + StateError("data never became ready in cubit '$runtimeType'")); + } + + Future setState(State newState) async { + try { + emit(AsyncValue.data(await store(newState))); + } on Exception catch (e, stackTrace) { + emit(AsyncValue.error(e, stackTrace)); + } + } +} diff --git a/lib/tools/async_value.dart b/lib/tools/async_value.dart new file mode 100644 index 0000000..8f02478 --- /dev/null +++ b/lib/tools/async_value.dart @@ -0,0 +1,172 @@ +// ignore_for_file: avoid_catches_without_on_clauses + +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'async_value.freezed.dart'; + +/// An utility for safely manipulating asynchronous data. +/// +/// By using [AsyncValue], you are guaranteed that you cannot forget to +/// handle the loading/error state of an asynchronous operation. +/// +/// It also expose some utilities to nicely convert an [AsyncValue] to +/// a different object. +/// For example, a Flutter Widget may use [when] to convert an [AsyncValue] +/// into either a progress indicator, an error screen, or to show the data: +/// +/// ```dart +/// /// A provider that asynchronously expose the current user +/// final userProvider = StreamProvider((_) async* { +/// // fetch the user +/// }); +/// +/// class Example extends ConsumerWidget { +/// @override +/// Widget build(BuildContext context, ScopedReader watch) { +/// final AsyncValue user = watch(userProvider); +/// +/// return user.when( +/// loading: () => CircularProgressIndicator(), +/// error: (error, stack) => Text('Oops, something unexpected happened'), +/// data: (value) => Text('Hello ${user.name}'), +/// ); +/// } +/// } +/// ``` +/// +/// If a consumer of an [AsyncValue] does not care about the loading/error +/// state, consider using [data] to read the state: +/// +/// ```dart +/// Widget build(BuildContext context, ScopedReader watch) { +/// // reads the data state directly – will be null during loading/error states +/// final User user = watch(userProvider).data?.value; +/// +/// return Text('Hello ${user?.name}'); +/// } +/// ``` +/// +/// See also: +/// +/// - [AsyncValue.guard], to simplify transforming a [Future] into an +/// [AsyncValue]. +/// - The package Freezed (https://github.com/rrousselgit/freezed), which have +/// generated this [AsyncValue] class and explains how [map]/[when] works. +@freezed +@sealed +abstract class AsyncValue with _$AsyncValue { + const AsyncValue._(); + + /// Creates an [AsyncValue] with a data. + /// + /// The data can be `null`. + const factory AsyncValue.data(T value) = AsyncData; + + /// Creates an [AsyncValue] in loading state. + /// + /// Prefer always using this constructor with the `const` keyword. + const factory AsyncValue.loading() = AsyncLoading; + + /// Creates an [AsyncValue] in error state. + /// + /// The parameter [error] cannot be `null`. + factory AsyncValue.error(Object error, [StackTrace? stackTrace]) = + AsyncError; + + /// Transforms a [Future] that may fail into something that is safe to read. + /// + /// This is useful to avoid having to do a tedious `try/catch`. Instead of: + /// + /// ```dart + /// class MyNotifier extends StateNotifier { + /// MyNotifier(): super(const AsyncValue.loading()) { + /// _fetchData(); + /// } + /// + /// Future _fetchData() async { + /// state = const AsyncValue.loading(); + /// try { + /// final response = await dio.get('my_api/data'); + /// final data = MyData.fromJson(response); + /// state = AsyncValue.data(data); + /// } catch (err, stack) { + /// state = AsyncValue.error(err, stack); + /// } + /// } + /// } + /// ``` + /// + /// which is redundant as the application grows and we need more and more of + /// this pattern – we can use [guard] to simplify it: + /// + /// + /// ```dart + /// class MyNotifier extends StateNotifier> { + /// MyNotifier(): super(const AsyncValue.loading()) { + /// _fetchData(); + /// } + /// + /// Future _fetchData() async { + /// state = const AsyncValue.loading(); + /// // does the try/catch for us like previously + /// state = await AsyncValue.guard(() async { + /// final response = await dio.get('my_api/data'); + /// return Data.fromJson(response); + /// }); + /// } + /// } + /// ``` + static Future> guard(Future Function() future) async { + try { + return AsyncValue.data(await future()); + } catch (err, stack) { + return AsyncValue.error(err, stack); + } + } + + /// The current data, or null if in loading/error. + /// + /// This is safe to use, as Dart (will) have non-nullable types. + /// As such reading [data] still forces to handle the loading/error cases + /// by having to check `data != null`. + /// + /// ## Why does [AsyncValue.data] return [AsyncData] instead of [T]? + /// + /// The motivation behind this decision is to allow differentiating between: + /// + /// - There is a data, and it is `null`. + /// ```dart + /// // There is a data, and it is "null" + /// AsyncValue configs = AsyncValue.data(null); + /// + /// print(configs.data); // AsyncValue(value: null) + /// print(configs.data.value); // null + /// ``` + /// + /// - There is no data. [AsyncValue] is currently in loading/error state. + /// ```dart + /// // No data, currently loading + /// AsyncValue configs = AsyncValue.loading(); + /// + /// print(configs.data); // null, currently loading + /// print(configs.data.value); // throws null exception + /// ``` + AsyncData? get data => map( + data: (data) => data, + loading: (_) => null, + error: (_) => null, + ); + + /// Shorthand for [when] to handle only the `data` case. + AsyncValue whenData(R Function(T value) cb) => when( + data: (value) { + try { + return AsyncValue.data(cb(value)); + } catch (err, stack) { + return AsyncValue.error(err, stack); + } + }, + loading: () => const AsyncValue.loading(), + error: AsyncValue.error, + ); +} diff --git a/lib/tools/async_value.freezed.dart b/lib/tools/async_value.freezed.dart new file mode 100644 index 0000000..2632704 --- /dev/null +++ b/lib/tools/async_value.freezed.dart @@ -0,0 +1,480 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'async_value.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +final _privateConstructorUsedError = UnsupportedError( + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#custom-getters-and-methods'); + +/// @nodoc +mixin _$AsyncValue { + @optionalTypeArgs + TResult when({ + required TResult Function(T value) data, + required TResult Function() loading, + required TResult Function(Object error, StackTrace? stackTrace) error, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function(T value)? data, + TResult? Function()? loading, + TResult? Function(Object error, StackTrace? stackTrace)? error, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult maybeWhen({ + TResult Function(T value)? data, + TResult Function()? loading, + TResult Function(Object error, StackTrace? stackTrace)? error, + required TResult orElse(), + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult map({ + required TResult Function(AsyncData value) data, + required TResult Function(AsyncLoading value) loading, + required TResult Function(AsyncError value) error, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(AsyncData value)? data, + TResult? Function(AsyncLoading value)? loading, + TResult? Function(AsyncError value)? error, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult maybeMap({ + TResult Function(AsyncData value)? data, + TResult Function(AsyncLoading value)? loading, + TResult Function(AsyncError value)? error, + required TResult orElse(), + }) => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $AsyncValueCopyWith { + factory $AsyncValueCopyWith( + AsyncValue value, $Res Function(AsyncValue) then) = + _$AsyncValueCopyWithImpl>; +} + +/// @nodoc +class _$AsyncValueCopyWithImpl> + implements $AsyncValueCopyWith { + _$AsyncValueCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; +} + +/// @nodoc +abstract class _$$AsyncDataImplCopyWith { + factory _$$AsyncDataImplCopyWith( + _$AsyncDataImpl value, $Res Function(_$AsyncDataImpl) then) = + __$$AsyncDataImplCopyWithImpl; + @useResult + $Res call({T value}); +} + +/// @nodoc +class __$$AsyncDataImplCopyWithImpl + extends _$AsyncValueCopyWithImpl> + implements _$$AsyncDataImplCopyWith { + __$$AsyncDataImplCopyWithImpl( + _$AsyncDataImpl _value, $Res Function(_$AsyncDataImpl) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? value = freezed, + }) { + return _then(_$AsyncDataImpl( + freezed == value + ? _value.value + : value // ignore: cast_nullable_to_non_nullable + as T, + )); + } +} + +/// @nodoc + +class _$AsyncDataImpl extends AsyncData { + const _$AsyncDataImpl(this.value) : super._(); + + @override + final T value; + + @override + String toString() { + return 'AsyncValue<$T>.data(value: $value)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$AsyncDataImpl && + const DeepCollectionEquality().equals(other.value, value)); + } + + @override + int get hashCode => + Object.hash(runtimeType, const DeepCollectionEquality().hash(value)); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$AsyncDataImplCopyWith> get copyWith => + __$$AsyncDataImplCopyWithImpl>(this, _$identity); + + @override + @optionalTypeArgs + TResult when({ + required TResult Function(T value) data, + required TResult Function() loading, + required TResult Function(Object error, StackTrace? stackTrace) error, + }) { + return data(value); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function(T value)? data, + TResult? Function()? loading, + TResult? Function(Object error, StackTrace? stackTrace)? error, + }) { + return data?.call(value); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function(T value)? data, + TResult Function()? loading, + TResult Function(Object error, StackTrace? stackTrace)? error, + required TResult orElse(), + }) { + if (data != null) { + return data(value); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map({ + required TResult Function(AsyncData value) data, + required TResult Function(AsyncLoading value) loading, + required TResult Function(AsyncError value) error, + }) { + return data(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(AsyncData value)? data, + TResult? Function(AsyncLoading value)? loading, + TResult? Function(AsyncError value)? error, + }) { + return data?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap({ + TResult Function(AsyncData value)? data, + TResult Function(AsyncLoading value)? loading, + TResult Function(AsyncError value)? error, + required TResult orElse(), + }) { + if (data != null) { + return data(this); + } + return orElse(); + } +} + +abstract class AsyncData extends AsyncValue { + const factory AsyncData(final T value) = _$AsyncDataImpl; + const AsyncData._() : super._(); + + T get value; + @JsonKey(ignore: true) + _$$AsyncDataImplCopyWith> get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class _$$AsyncLoadingImplCopyWith { + factory _$$AsyncLoadingImplCopyWith(_$AsyncLoadingImpl value, + $Res Function(_$AsyncLoadingImpl) then) = + __$$AsyncLoadingImplCopyWithImpl; +} + +/// @nodoc +class __$$AsyncLoadingImplCopyWithImpl + extends _$AsyncValueCopyWithImpl> + implements _$$AsyncLoadingImplCopyWith { + __$$AsyncLoadingImplCopyWithImpl( + _$AsyncLoadingImpl _value, $Res Function(_$AsyncLoadingImpl) _then) + : super(_value, _then); +} + +/// @nodoc + +class _$AsyncLoadingImpl extends AsyncLoading { + const _$AsyncLoadingImpl() : super._(); + + @override + String toString() { + return 'AsyncValue<$T>.loading()'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && other is _$AsyncLoadingImpl); + } + + @override + int get hashCode => runtimeType.hashCode; + + @override + @optionalTypeArgs + TResult when({ + required TResult Function(T value) data, + required TResult Function() loading, + required TResult Function(Object error, StackTrace? stackTrace) error, + }) { + return loading(); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function(T value)? data, + TResult? Function()? loading, + TResult? Function(Object error, StackTrace? stackTrace)? error, + }) { + return loading?.call(); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function(T value)? data, + TResult Function()? loading, + TResult Function(Object error, StackTrace? stackTrace)? error, + required TResult orElse(), + }) { + if (loading != null) { + return loading(); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map({ + required TResult Function(AsyncData value) data, + required TResult Function(AsyncLoading value) loading, + required TResult Function(AsyncError value) error, + }) { + return loading(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(AsyncData value)? data, + TResult? Function(AsyncLoading value)? loading, + TResult? Function(AsyncError value)? error, + }) { + return loading?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap({ + TResult Function(AsyncData value)? data, + TResult Function(AsyncLoading value)? loading, + TResult Function(AsyncError value)? error, + required TResult orElse(), + }) { + if (loading != null) { + return loading(this); + } + return orElse(); + } +} + +abstract class AsyncLoading extends AsyncValue { + const factory AsyncLoading() = _$AsyncLoadingImpl; + const AsyncLoading._() : super._(); +} + +/// @nodoc +abstract class _$$AsyncErrorImplCopyWith { + factory _$$AsyncErrorImplCopyWith( + _$AsyncErrorImpl value, $Res Function(_$AsyncErrorImpl) then) = + __$$AsyncErrorImplCopyWithImpl; + @useResult + $Res call({Object error, StackTrace? stackTrace}); +} + +/// @nodoc +class __$$AsyncErrorImplCopyWithImpl + extends _$AsyncValueCopyWithImpl> + implements _$$AsyncErrorImplCopyWith { + __$$AsyncErrorImplCopyWithImpl( + _$AsyncErrorImpl _value, $Res Function(_$AsyncErrorImpl) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? error = null, + Object? stackTrace = freezed, + }) { + return _then(_$AsyncErrorImpl( + null == error ? _value.error : error, + freezed == stackTrace + ? _value.stackTrace + : stackTrace // ignore: cast_nullable_to_non_nullable + as StackTrace?, + )); + } +} + +/// @nodoc + +class _$AsyncErrorImpl extends AsyncError { + _$AsyncErrorImpl(this.error, [this.stackTrace]) : super._(); + + @override + final Object error; + @override + final StackTrace? stackTrace; + + @override + String toString() { + return 'AsyncValue<$T>.error(error: $error, stackTrace: $stackTrace)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$AsyncErrorImpl && + const DeepCollectionEquality().equals(other.error, error) && + (identical(other.stackTrace, stackTrace) || + other.stackTrace == stackTrace)); + } + + @override + int get hashCode => Object.hash( + runtimeType, const DeepCollectionEquality().hash(error), stackTrace); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$AsyncErrorImplCopyWith> get copyWith => + __$$AsyncErrorImplCopyWithImpl>(this, _$identity); + + @override + @optionalTypeArgs + TResult when({ + required TResult Function(T value) data, + required TResult Function() loading, + required TResult Function(Object error, StackTrace? stackTrace) error, + }) { + return error(this.error, stackTrace); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function(T value)? data, + TResult? Function()? loading, + TResult? Function(Object error, StackTrace? stackTrace)? error, + }) { + return error?.call(this.error, stackTrace); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function(T value)? data, + TResult Function()? loading, + TResult Function(Object error, StackTrace? stackTrace)? error, + required TResult orElse(), + }) { + if (error != null) { + return error(this.error, stackTrace); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map({ + required TResult Function(AsyncData value) data, + required TResult Function(AsyncLoading value) loading, + required TResult Function(AsyncError value) error, + }) { + return error(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(AsyncData value)? data, + TResult? Function(AsyncLoading value)? loading, + TResult? Function(AsyncError value)? error, + }) { + return error?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap({ + TResult Function(AsyncData value)? data, + TResult Function(AsyncLoading value)? loading, + TResult Function(AsyncError value)? error, + required TResult orElse(), + }) { + if (error != null) { + return error(this); + } + return orElse(); + } +} + +abstract class AsyncError extends AsyncValue { + factory AsyncError(final Object error, [final StackTrace? stackTrace]) = + _$AsyncErrorImpl; + AsyncError._() : super._(); + + Object get error; + StackTrace? get stackTrace; + @JsonKey(ignore: true) + _$$AsyncErrorImplCopyWith> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/lib/tools/loggy.dart b/lib/tools/loggy.dart index fc07d3f..0a7f6a5 100644 --- a/lib/tools/loggy.dart +++ b/lib/tools/loggy.dart @@ -1,13 +1,15 @@ import 'dart:io' show Platform; import 'package:ansicolor/ansicolor.dart'; +import 'package:bloc/bloc.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter_translate/flutter_translate.dart'; import 'package:intl/intl.dart'; import 'package:loggy/loggy.dart'; -import '../pages/developer.dart'; +import '../old_to_refactor/pages/developer.dart'; import '../veilid_support/veilid_support.dart'; +import 'state_logger.dart'; String wrapWithLogColor(LogLevel? level, String text) { // XXX: https://github.com/flutter/flutter/issues/64491 @@ -149,4 +151,7 @@ void initLoggy() { } Loggy('').level = getLogOptions(logLevel); + + // Create state logger + Bloc.observer = const StateLogger(); } diff --git a/lib/tools/secret_crypto.dart b/lib/tools/secret_crypto.dart index 873932d..262cadd 100644 --- a/lib/tools/secret_crypto.dart +++ b/lib/tools/secret_crypto.dart @@ -1,5 +1,5 @@ import 'dart:typed_data'; -import '../entities/local_account.dart'; +import '../local_accounts/local_account.dart'; import '../veilid_init.dart'; import '../veilid_support/veilid_support.dart'; diff --git a/lib/tools/stack_trace.dart b/lib/tools/stack_trace.dart new file mode 100644 index 0000000..ec21f24 --- /dev/null +++ b/lib/tools/stack_trace.dart @@ -0,0 +1,12 @@ +import 'package:stack_trace/stack_trace.dart'; + +/// Rethrows [error] with a stacktrace that is the combination of [stackTrace] +/// and [StackTrace.current]. +Never throwErrorWithCombinedStackTrace(Object error, StackTrace stackTrace) { + final chain = Chain([ + Trace.current(), + ...Chain.forTrace(stackTrace).traces, + ]); // .foldFrames((frame) => frame.package == 'riverpod'); + + Error.throwWithStackTrace(error, chain.toTrace().vmTrace); +} diff --git a/lib/tools/state_logger.dart b/lib/tools/state_logger.dart index 973b5f9..28230a8 100644 --- a/lib/tools/state_logger.dart +++ b/lib/tools/state_logger.dart @@ -1,22 +1,33 @@ -import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:bloc/bloc.dart'; import 'loggy.dart'; -class StateLogger extends ProviderObserver { +/// [BlocObserver] for the VeilidChat application that +/// observes all state changes. +class StateLogger extends BlocObserver { + /// {@macro counter_observer} const StateLogger(); + @override - void didUpdateProvider( - ProviderBase provider, - Object? previousValue, - Object? newValue, - ProviderContainer container, - ) { - log.debug(''' -{ - provider: ${provider.name ?? provider.runtimeType}, - oldValue: $previousValue, - newValue: $newValue -} -'''); - super.didUpdateProvider(provider, previousValue, newValue, container); + void onChange(BlocBase bloc, Change change) { + super.onChange(bloc, change); + log.debug('Change: ${bloc.runtimeType} $change'); + } + + @override + void onCreate(BlocBase bloc) { + super.onCreate(bloc); + log.debug('Create: ${bloc.runtimeType}'); + } + + @override + void onClose(BlocBase bloc) { + super.onClose(bloc); + log.debug('Close: ${bloc.runtimeType}'); + } + + @override + void onError(BlocBase bloc, Object error, StackTrace stackTrace) { + super.onError(bloc, error, stackTrace); + log.error('Error: ${bloc.runtimeType} $error\n$stackTrace'); } } diff --git a/lib/tools/stream_listenable.dart b/lib/tools/stream_listenable.dart new file mode 100644 index 0000000..f01ee04 --- /dev/null +++ b/lib/tools/stream_listenable.dart @@ -0,0 +1,34 @@ +import 'dart:async'; + +import 'package:flutter/foundation.dart'; + +import 'loggy.dart'; + +/// Converts a [Stream] into a [Listenable] +/// +/// {@tool snippet} +/// Typical usage is as follows: +/// +/// ```dart +/// StreamListenable(stream) +/// ``` +/// {@end-tool} +class StreamListenable extends ChangeNotifier { + /// Creates a [StreamListenable]. + /// + /// Every time the [Stream] receives an event this [ChangeNotifier] will + /// notify its listeners. + StreamListenable(Stream stream) { + notifyListeners(); + _subscription = stream.asBroadcastStream().listen((_) => notifyListeners()); + } + + late final StreamSubscription _subscription; + + @override + void dispose() { + unawaited(_subscription.cancel().onError((error, stackTrace) => + log.error('StreamListenable cancel error: $error\n$stackTrace'))); + super.dispose(); + } +} diff --git a/lib/tools/theme_service.dart b/lib/tools/theme_service.dart deleted file mode 100644 index 41b664e..0000000 --- a/lib/tools/theme_service.dart +++ /dev/null @@ -1,255 +0,0 @@ -// ignore_for_file: always_put_required_named_parameters_first - -import 'dart:convert'; - -import 'package:flutter/material.dart'; -import 'package:riverpod_annotation/riverpod_annotation.dart'; -import 'package:shared_preferences/shared_preferences.dart'; - -import '../entities/preferences.dart'; -import 'radix_generator.dart'; - -part 'theme_service.g.dart'; - -class ScaleColor { - ScaleColor({ - required this.appBackground, - required this.subtleBackground, - required this.elementBackground, - required this.hoverElementBackground, - required this.activeElementBackground, - required this.subtleBorder, - required this.border, - required this.hoverBorder, - required this.background, - required this.hoverBackground, - required this.subtleText, - required this.text, - }); - - Color appBackground; - Color subtleBackground; - Color elementBackground; - Color hoverElementBackground; - Color activeElementBackground; - Color subtleBorder; - Color border; - Color hoverBorder; - Color background; - Color hoverBackground; - Color subtleText; - Color text; - - ScaleColor copyWith( - {Color? appBackground, - Color? subtleBackground, - Color? elementBackground, - Color? hoverElementBackground, - Color? activeElementBackground, - 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, - activeElementBackground: - activeElementBackground ?? this.activeElementBackground, - 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), - activeElementBackground: Color.lerp( - a.activeElementBackground, b.activeElementBackground, 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 extends ThemeExtension { - ScaleScheme( - {required this.primaryScale, - required this.primaryAlphaScale, - required this.secondaryScale, - required this.tertiaryScale, - required this.grayScale, - required this.errorScale}); - - final ScaleColor primaryScale; - final ScaleColor primaryAlphaScale; - final ScaleColor secondaryScale; - final ScaleColor tertiaryScale; - final ScaleColor grayScale; - final ScaleColor errorScale; - - @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), - ); - } -} - -//////////////////////////////////////////////////////////////////////// - -class ThemeService { - ThemeService._(); - static late SharedPreferences prefs; - static ThemeService? _instance; - - static Future get instance async { - if (_instance == null) { - prefs = await SharedPreferences.getInstance(); - _instance = ThemeService._(); - } - return _instance!; - } - - static bool get isPlatformDark => - WidgetsBinding.instance.platformDispatcher.platformBrightness == - Brightness.dark; - - ThemeData get initial { - final themePreferences = load(); - return get(themePreferences); - } - - ThemePreferences load() { - final themePreferencesJson = prefs.getString('themePreferences'); - ThemePreferences? themePreferences; - if (themePreferencesJson != null) { - try { - themePreferences = - ThemePreferences.fromJson(jsonDecode(themePreferencesJson)); - // ignore: avoid_catches_without_on_clauses - } catch (_) { - // ignore - } - } - return themePreferences ?? - const ThemePreferences( - colorPreference: ColorPreference.vapor, - brightnessPreference: BrightnessPreference.system, - displayScale: 1, - ); - } - - Future save(ThemePreferences themePreferences) async { - await prefs.setString( - 'themePreferences', jsonEncode(themePreferences.toJson())); - } - - ThemeData get(ThemePreferences themePreferences) { - late final Brightness brightness; - switch (themePreferences.brightnessPreference) { - case BrightnessPreference.system: - if (isPlatformDark) { - brightness = Brightness.dark; - } else { - brightness = Brightness.light; - } - case BrightnessPreference.light: - brightness = Brightness.light; - case BrightnessPreference.dark: - brightness = Brightness.dark; - } - - late final ThemeData themeData; - switch (themePreferences.colorPreference) { - // Special cases - case ColorPreference.contrast: - // xxx do contrastGenerator - themeData = radixGenerator(brightness, RadixThemeColor.grim); - // Generate from Radix - case ColorPreference.scarlet: - themeData = radixGenerator(brightness, RadixThemeColor.scarlet); - case ColorPreference.babydoll: - themeData = radixGenerator(brightness, RadixThemeColor.babydoll); - case ColorPreference.vapor: - themeData = radixGenerator(brightness, RadixThemeColor.vapor); - case ColorPreference.gold: - themeData = radixGenerator(brightness, RadixThemeColor.gold); - case ColorPreference.garden: - themeData = radixGenerator(brightness, RadixThemeColor.garden); - case ColorPreference.forest: - themeData = radixGenerator(brightness, RadixThemeColor.forest); - case ColorPreference.arctic: - themeData = radixGenerator(brightness, RadixThemeColor.arctic); - case ColorPreference.lapis: - themeData = radixGenerator(brightness, RadixThemeColor.lapis); - case ColorPreference.eggplant: - themeData = radixGenerator(brightness, RadixThemeColor.eggplant); - case ColorPreference.lime: - themeData = radixGenerator(brightness, RadixThemeColor.lime); - case ColorPreference.grim: - themeData = radixGenerator(brightness, RadixThemeColor.grim); - } - - return themeData; - } -} - -@riverpod -FutureOr themeService(ThemeServiceRef ref) async => - await ThemeService.instance; diff --git a/lib/tools/theme_service.g.dart b/lib/tools/theme_service.g.dart deleted file mode 100644 index e146df9..0000000 --- a/lib/tools/theme_service.g.dart +++ /dev/null @@ -1,24 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'theme_service.dart'; - -// ************************************************************************** -// RiverpodGenerator -// ************************************************************************** - -String _$themeServiceHash() => r'87dbacb9df4923f507fb01e486b91d73a3fcef9c'; - -/// See also [themeService]. -@ProviderFor(themeService) -final themeServiceProvider = AutoDisposeFutureProvider.internal( - themeService, - name: r'themeServiceProvider', - debugGetCreateSourceHash: - const bool.fromEnvironment('dart.vm.product') ? null : _$themeServiceHash, - dependencies: null, - allTransitiveDependencies: null, -); - -typedef ThemeServiceRef = AutoDisposeFutureProviderRef; -// ignore_for_file: type=lint -// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member diff --git a/lib/tools/tools.dart b/lib/tools/tools.dart index 11cb944..34af70d 100644 --- a/lib/tools/tools.dart +++ b/lib/tools/tools.dart @@ -1,10 +1,10 @@ export 'animations.dart'; +export 'async_table_db_backed_cubit.dart'; +export 'async_value.dart'; export 'loggy.dart'; export 'phono_byte.dart'; -export 'radix_generator.dart'; export 'responsive.dart'; export 'scanner_error_widget.dart'; export 'secret_crypto.dart'; export 'state_logger.dart'; -export 'theme_service.dart'; export 'widget_helpers.dart'; diff --git a/lib/tools/widget_helpers.dart b/lib/tools/widget_helpers.dart index 0d05d27..618f377 100644 --- a/lib/tools/widget_helpers.dart +++ b/lib/tools/widget_helpers.dart @@ -6,7 +6,7 @@ import 'package:flutter_translate/flutter_translate.dart'; import 'package:motion_toast/motion_toast.dart'; import 'package:quickalert/quickalert.dart'; -import 'theme_service.dart'; +import '../theme/theme.dart'; extension BorderExt on Widget { DecoratedBox debugBorder() => DecoratedBox( diff --git a/lib/veilid_init.dart b/lib/veilid_init.dart deleted file mode 100644 index 86c0900..0000000 --- a/lib/veilid_init.dart +++ /dev/null @@ -1,78 +0,0 @@ -import 'dart:async'; - -import 'package:flutter/foundation.dart'; -import 'package:riverpod_annotation/riverpod_annotation.dart'; - -import 'processor.dart'; -import 'veilid_support/veilid_support.dart'; - -part 'veilid_init.g.dart'; - -Future getVeilidVersion() async { - String veilidVersion; - try { - veilidVersion = Veilid.instance.veilidVersionString(); - } on Exception { - veilidVersion = 'Failed to get veilid version.'; - } - return veilidVersion; -} - -// Initialize Veilid -// Call only once. -void _initVeilid() { - if (kIsWeb) { - const platformConfig = VeilidWASMConfig( - logging: VeilidWASMConfigLogging( - performance: VeilidWASMConfigLoggingPerformance( - enabled: true, - level: VeilidConfigLogLevel.debug, - logsInTimings: true, - logsInConsole: false), - api: VeilidWASMConfigLoggingApi( - enabled: true, level: VeilidConfigLogLevel.info))); - Veilid.instance.initializeVeilidCore(platformConfig.toJson()); - } else { - const platformConfig = VeilidFFIConfig( - logging: VeilidFFIConfigLogging( - terminal: VeilidFFIConfigLoggingTerminal( - enabled: false, - level: VeilidConfigLogLevel.debug, - ), - otlp: VeilidFFIConfigLoggingOtlp( - enabled: false, - level: VeilidConfigLogLevel.trace, - grpcEndpoint: '127.0.0.1:4317', - serviceName: 'VeilidChat'), - api: VeilidFFIConfigLoggingApi( - enabled: true, level: VeilidConfigLogLevel.info))); - Veilid.instance.initializeVeilidCore(platformConfig.toJson()); - } -} - -Completer eventualVeilid = Completer(); -Processor processor = Processor(); - -Future initializeVeilid() async { - // Ensure this runs only once - if (eventualVeilid.isCompleted) { - return; - } - - // Init Veilid - _initVeilid(); - - // Veilid logging - initVeilidLog(); - - // Startup Veilid - await processor.startup(); - - // Share the initialized veilid instance to the rest of the app - eventualVeilid.complete(Veilid.instance); -} - -// Expose the Veilid instance as a FutureProvider -@riverpod -FutureOr veilidInstance(VeilidInstanceRef ref) async => - await eventualVeilid.future; diff --git a/lib/veilid_init.g.dart b/lib/veilid_init.g.dart deleted file mode 100644 index ab235f9..0000000 --- a/lib/veilid_init.g.dart +++ /dev/null @@ -1,25 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'veilid_init.dart'; - -// ************************************************************************** -// RiverpodGenerator -// ************************************************************************** - -String _$veilidInstanceHash() => r'cca5cf288bafc4a051a1713e285f4c1d3ef4b680'; - -/// See also [veilidInstance]. -@ProviderFor(veilidInstance) -final veilidInstanceProvider = AutoDisposeFutureProvider.internal( - veilidInstance, - name: r'veilidInstanceProvider', - debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') - ? null - : _$veilidInstanceHash, - dependencies: null, - allTransitiveDependencies: null, -); - -typedef VeilidInstanceRef = AutoDisposeFutureProviderRef; -// ignore_for_file: type=lint -// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member diff --git a/lib/veilid_support/dht_support/src/dht_record_pool.dart b/lib/veilid_support/dht_support/src/dht_record_pool.dart index cbd879e..88edf37 100644 --- a/lib/veilid_support/dht_support/src/dht_record_pool.dart +++ b/lib/veilid_support/dht_support/src/dht_record_pool.dart @@ -35,7 +35,7 @@ class OwnedDHTRecordPointer with _$OwnedDHTRecordPointer { _$OwnedDHTRecordPointerFromJson(json as Map); } -class DHTRecordPool with AsyncTableDBBacked { +class DHTRecordPool with TableDBBacked { DHTRecordPool._(Veilid veilid, VeilidRoutingContext routingContext) : _state = DHTRecordPoolAllocations( childrenByParent: IMap(), diff --git a/lib/veilid_support/src/config.dart b/lib/veilid_support/src/config.dart index e45cfe7..3e3aa76 100644 --- a/lib/veilid_support/src/config.dart +++ b/lib/veilid_support/src/config.dart @@ -1,8 +1,37 @@ import 'package:flutter/foundation.dart'; import 'package:veilid/veilid.dart'; -Future getVeilidChatConfig() async { - var config = await getDefaultVeilidConfig('VeilidChat'); +Map getDefaultVeilidPlatformConfig(String appName) { + if (kIsWeb) { + return const VeilidWASMConfig( + logging: VeilidWASMConfigLogging( + performance: VeilidWASMConfigLoggingPerformance( + enabled: true, + level: VeilidConfigLogLevel.debug, + logsInTimings: true, + logsInConsole: false), + api: VeilidWASMConfigLoggingApi( + enabled: true, level: VeilidConfigLogLevel.info))) + .toJson(); + } + return VeilidFFIConfig( + logging: VeilidFFIConfigLogging( + terminal: const VeilidFFIConfigLoggingTerminal( + enabled: false, + level: VeilidConfigLogLevel.debug, + ), + otlp: VeilidFFIConfigLoggingOtlp( + enabled: false, + level: VeilidConfigLogLevel.trace, + grpcEndpoint: '127.0.0.1:4317', + serviceName: appName), + api: const VeilidFFIConfigLoggingApi( + enabled: true, level: VeilidConfigLogLevel.info))) + .toJson(); +} + +Future getVeilidConfig(String appName) async { + var config = await getDefaultVeilidConfig(appName); // ignore: do_not_use_environment if (const String.fromEnvironment('DELETE_TABLE_STORE') == '1') { config = diff --git a/lib/veilid_support/src/table_db.dart b/lib/veilid_support/src/table_db.dart index a20b4be..756c7db 100644 --- a/lib/veilid_support/src/table_db.dart +++ b/lib/veilid_support/src/table_db.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:veilid/veilid.dart'; Future tableScope( @@ -29,7 +31,7 @@ Future transactionScope( } } -abstract mixin class AsyncTableDBBacked { +abstract mixin class TableDBBacked { String tableName(); String tableKeyName(); T valueFromJson(Object? obj); @@ -52,3 +54,48 @@ abstract mixin class AsyncTableDBBacked { return obj; } } + +class TableDBValue extends TableDBBacked { + TableDBValue({ + required String tableName, + required String tableKeyName, + required T Function(Object? obj) valueFromJson, + required Object? Function(T obj) valueToJson, + }) : _tableName = tableName, + _valueFromJson = valueFromJson, + _valueToJson = valueToJson, + _tableKeyName = tableKeyName; + + T? get value => _value; + T get requireValue => _value!; + + Future get() async { + final val = _value; + if (val != null) { + return val; + } + final loadedValue = await load(); + return _value = loadedValue; + } + + Future set(T newVal) async { + _value = await store(newVal); + } + + T? _value; + final String _tableName; + final String _tableKeyName; + final T Function(Object? obj) _valueFromJson; + final Object? Function(T obj) _valueToJson; + + ////////////////////////////////////////////////////////////// + /// AsyncTableDBBacked + @override + String tableName() => _tableName; + @override + String tableKeyName() => _tableKeyName; + @override + T valueFromJson(Object? obj) => _valueFromJson(obj); + @override + Object? valueToJson(T val) => _valueToJson(val); +} diff --git a/pubspec.lock b/pubspec.lock index ffbd7a1..72af3e7 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -17,14 +17,6 @@ packages: url: "https://pub.dev" source: hosted version: "6.2.0" - analyzer_plugin: - dependency: transitive - description: - name: analyzer_plugin - sha256: "9661b30b13a685efaee9f02e5d01ed9f2b423bd889d28a304d02d704aee69161" - url: "https://pub.dev" - source: hosted - version: "0.11.3" animated_theme_switcher: dependency: "direct main" description: @@ -89,6 +81,14 @@ packages: url: "https://pub.dev" source: hosted version: "5.7.0" + bloc: + dependency: "direct main" + description: + name: bloc + sha256: "3820f15f502372d979121de1f6b97bfcf1630ebff8fe1d52fb2b0bfa49be5b49" + url: "https://pub.dev" + source: hosted + version: "8.1.2" blurry_modal_progress_hud: dependency: "direct main" description: @@ -273,14 +273,6 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.3" - ci: - dependency: transitive - description: - name: ci - sha256: "145d095ce05cddac4d797a158bc4cf3b6016d1fe63d8c3d2fbd7212590adca13" - url: "https://pub.dev" - source: hosted - version: "0.1.0" circular_profile_avatar: dependency: "direct main" description: @@ -297,14 +289,6 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.1" - cli_util: - dependency: transitive - description: - name: cli_util - sha256: c05b7406fdabc7a49a3929d4af76bcaccbbffcbcdcf185b082e1ae07da323d19 - url: "https://pub.dev" - source: hosted - version: "0.4.1" clock: dependency: transitive description: @@ -377,22 +361,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.6" - custom_lint: - dependency: transitive - description: - name: custom_lint - sha256: "198ec6b8e084d22f508a76556c9afcfb71706ad3f42b083fe0ee923351a96d90" - url: "https://pub.dev" - source: hosted - version: "0.5.7" - custom_lint_core: - dependency: transitive - description: - name: custom_lint_core - sha256: f84c3fe2f27ef3b8831953e477e59d4a29c2952623f9eac450d7b40d9cdd94cc - url: "https://pub.dev" - source: hosted - version: "0.5.7" dart_style: dependency: transitive description: @@ -478,6 +446,14 @@ packages: url: "https://pub.dev" source: hosted version: "4.3.0" + flutter_bloc: + dependency: "direct main" + description: + name: flutter_bloc + sha256: e74efb89ee6945bcbce74a5b3a5a3376b088e5f21f55c263fc38cbdc6237faae + url: "https://pub.dev" + source: hosted + version: "8.1.3" flutter_cache_manager: dependency: transitive description: @@ -563,14 +539,6 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.17" - flutter_riverpod: - dependency: "direct main" - description: - name: flutter_riverpod - sha256: da9591d1f8d5881628ccd5c25c40e74fc3eef50ba45e40c3905a06e1712412d5 - url: "https://pub.dev" - source: hosted - version: "2.4.9" flutter_slidable: dependency: "direct main" description: @@ -677,14 +645,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.3.1" - hooks_riverpod: - dependency: "direct main" + hive: + dependency: transitive description: - name: hooks_riverpod - sha256: c12a456e03ef9be65b0be66963596650ad7a3220e96c7e7b0a048562ea32d6ae + name: hive + sha256: "8dcf6db979d7933da8217edcec84e9df1bdb4e4edc7fc77dbd5aa74356d6d941" url: "https://pub.dev" source: hosted - version: "2.4.9" + version: "2.2.3" html: dependency: transitive description: @@ -717,6 +685,14 @@ packages: url: "https://pub.dev" source: hosted version: "4.0.2" + hydrated_bloc: + dependency: "direct main" + description: + name: hydrated_bloc + sha256: c925e49704c052a8f249226ae7603f86bfa776b910816390763b956c71d2cbaf + url: "https://pub.dev" + source: hosted + version: "9.1.3" icons_launcher: dependency: "direct dev" description: @@ -822,7 +798,7 @@ packages: source: hosted version: "0.5.0" meta: - dependency: transitive + dependency: "direct main" description: name: meta sha256: a6e590c838b18133bb482a2745ad77c5bb7715fb0451209e1a7567d416678b8e @@ -861,6 +837,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.1.0" + nested: + dependency: transitive + description: + name: nested + sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20" + url: "https://pub.dev" + source: hosted + version: "1.0.0" octo_image: dependency: transitive description: @@ -1029,6 +1013,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.1.0" + provider: + dependency: transitive + description: + name: provider + sha256: "9a96a0a19b594dbc5bf0f1f27d2bc67d5f95957359b461cd9feb44ed6ae75096" + url: "https://pub.dev" + source: hosted + version: "6.1.1" pub_semver: dependency: transitive description: @@ -1101,38 +1093,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.10" - riverpod: - dependency: transitive - description: - name: riverpod - sha256: "942999ee48b899f8a46a860f1e13cee36f2f77609eb54c5b7a669bb20d550b11" - url: "https://pub.dev" - source: hosted - version: "2.4.9" - riverpod_analyzer_utils: - dependency: transitive - description: - name: riverpod_analyzer_utils - sha256: d4dabc35358413bf4611fcb6abb46308a67c4ef4cd5e69fd3367b11925c59f57 - url: "https://pub.dev" - source: hosted - version: "0.5.0" - riverpod_annotation: - dependency: "direct main" - description: - name: riverpod_annotation - sha256: b70e95fbd5ca7ce42f5148092022971bb2e9843b6ab71e97d479e8ab52e98979 - url: "https://pub.dev" - source: hosted - version: "2.3.3" - riverpod_generator: - dependency: "direct dev" - description: - name: riverpod_generator - sha256: ff8f064f1d7ef3cc6af481bba8e9a3fcdb4d34df34fac1b39bbc003167065be0 - url: "https://pub.dev" - source: hosted - version: "2.3.9" rxdart: dependency: transitive description: @@ -1323,21 +1283,13 @@ packages: source: hosted version: "2.5.0+2" stack_trace: - dependency: transitive + dependency: "direct main" description: name: stack_trace sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" url: "https://pub.dev" source: hosted version: "1.11.1" - state_notifier: - dependency: transitive - description: - name: state_notifier - sha256: b8677376aa54f2d7c58280d5a007f9e8774f1968d1fb1c096adcb4792fba29bb - url: "https://pub.dev" - source: hosted - version: "1.0.0" stream_channel: dependency: transitive description: @@ -1347,7 +1299,7 @@ packages: source: hosted version: "2.1.2" stream_transform: - dependency: transitive + dependency: "direct main" description: name: stream_transform sha256: "14a00e794c7c11aa145a170587321aedce29769c08d7f58b1d141da75e3b1c6f" diff --git a/pubspec.yaml b/pubspec.yaml index 6a58ced..b061813 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -14,6 +14,7 @@ dependencies: awesome_extensions: ^2.0.9 badges: ^3.1.1 basic_utils: ^5.6.1 + bloc: ^8.1.2 blurry_modal_progress_hud: ^1.1.0 change_case: ^1.1.0 charcode: ^1.3.1 @@ -27,6 +28,7 @@ dependencies: flutter: sdk: flutter flutter_animate: ^4.2.0+1 + flutter_bloc: ^8.1.3 flutter_chat_types: ^3.6.2 flutter_chat_ui: ^1.6.9 flutter_form_builder: ^9.1.0 @@ -34,7 +36,6 @@ dependencies: flutter_localizations: sdk: flutter flutter_native_splash: ^2.3.2 - flutter_riverpod: ^2.1.3 flutter_slidable: ^3.0.0 flutter_spinkit: ^5.2.0 flutter_svg: ^2.0.7 @@ -42,11 +43,12 @@ dependencies: form_builder_validators: ^9.0.0 freezed_annotation: ^2.2.0 go_router: ^11.0.0 - hooks_riverpod: ^2.1.3 + hydrated_bloc: ^9.1.3 image: ^4.1.3 intl: ^0.18.0 json_annotation: ^4.8.1 loggy: ^2.0.3 + meta: ^1.10.0 mobile_scanner: ^3.5.1 motion_toast: ^2.7.8 mutex: ^3.0.1 @@ -61,12 +63,13 @@ dependencies: quickalert: ^1.0.1 radix_colors: ^1.0.4 reorderable_grid: ^1.0.7 - riverpod_annotation: ^2.1.1 searchable_listview: ^2.7.0 share_plus: ^7.0.2 shared_preferences: ^2.0.15 signal_strength_indicator: ^0.4.1 split_view: ^3.2.1 + stack_trace: ^1.11.1 + stream_transform: ^2.1.0 stylish_bottom_bar: ^1.0.3 uuid: ^3.0.7 veilid: @@ -84,7 +87,6 @@ dev_dependencies: icons_launcher: ^2.1.3 json_serializable: ^6.7.1 lint_hard: ^4.0.0 - riverpod_generator: ^2.2.3 flutter_native_splash: color: "#8588D0" From 2adc958128199e896457ae909cb83fb5672ada76 Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Tue, 26 Dec 2023 20:27:47 -0500 Subject: [PATCH 04/68] missing changes --- lib/main.dart | 1 - lib/old_to_refactor/providers/conversation.dart | 2 +- lib/processor.dart | 2 +- lib/tick.dart | 2 +- lib/tools/async_table_db_backed_cubit.dart | 2 +- lib/tools/secret_crypto.dart | 2 +- 6 files changed, 5 insertions(+), 6 deletions(-) diff --git a/lib/main.dart b/lib/main.dart index 7cd828c..522bd00 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -13,7 +13,6 @@ import 'theme/theme.dart'; import 'tools/tools.dart'; import 'init.dart'; -const String appName = "VeilidChat"; void main() async { // Disable all debugprints in release mode diff --git a/lib/old_to_refactor/providers/conversation.dart b/lib/old_to_refactor/providers/conversation.dart index 638e1ec..aedd01b 100644 --- a/lib/old_to_refactor/providers/conversation.dart +++ b/lib/old_to_refactor/providers/conversation.dart @@ -10,7 +10,7 @@ import 'package:riverpod_annotation/riverpod_annotation.dart'; import '../../proto/proto.dart' as proto; import '../../tools/tools.dart'; -import '../../veilid_init.dart'; +import '../../init.dart'; import '../../veilid_support/veilid_support.dart'; import 'account.dart'; import 'chat.dart'; diff --git a/lib/processor.dart b/lib/processor.dart index 5cb7c89..36878fd 100644 --- a/lib/processor.dart +++ b/lib/processor.dart @@ -33,7 +33,7 @@ class Processor { } on Exception {} final updateStream = - await Veilid.instance.startupVeilidCore(await getVeilidChatConfig()); + await Veilid.instance.startupVeilidCore(await getVeilidConfig()); _updateStream = updateStream; _updateProcessor = processUpdates(); _startedUp = true; diff --git a/lib/tick.dart b/lib/tick.dart index 55eecbf..bcaa478 100644 --- a/lib/tick.dart +++ b/lib/tick.dart @@ -12,7 +12,7 @@ import 'old_to_refactor/providers/connection_state.dart'; import 'old_to_refactor/providers/contact.dart'; import 'old_to_refactor/providers/contact_invite.dart'; import 'old_to_refactor/providers/conversation.dart'; -import 'veilid_init.dart'; +import 'init.dart'; const int ticksPerContactInvitationCheck = 5; const int ticksPerNewMessageCheck = 5; diff --git a/lib/tools/async_table_db_backed_cubit.dart b/lib/tools/async_table_db_backed_cubit.dart index a4ca440..b3aac2c 100644 --- a/lib/tools/async_table_db_backed_cubit.dart +++ b/lib/tools/async_table_db_backed_cubit.dart @@ -3,7 +3,7 @@ import 'dart:async'; import 'package:bloc/bloc.dart'; import '../../tools/tools.dart'; -import '../../veilid_init.dart'; +import '../init.dart'; import '../../veilid_support/veilid_support.dart'; abstract class AsyncTableDBBackedCubit extends Cubit> diff --git a/lib/tools/secret_crypto.dart b/lib/tools/secret_crypto.dart index 262cadd..0920be7 100644 --- a/lib/tools/secret_crypto.dart +++ b/lib/tools/secret_crypto.dart @@ -1,6 +1,6 @@ import 'dart:typed_data'; import '../local_accounts/local_account.dart'; -import '../veilid_init.dart'; +import '../init.dart'; import '../veilid_support/veilid_support.dart'; Future encryptSecretToBytes( From c516323e7dae73bff2324dd85c2cd2ec8ad28b2d Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Wed, 27 Dec 2023 22:56:24 -0500 Subject: [PATCH 05/68] refactor some more --- build.sh | 4 +- lib/account_manager/account_manager.dart | 3 + .../active_user_login_cubit.dart | 6 +- .../active_user_login_state.dart | 0 .../cubit/cubit.dart} | 2 +- .../local_accounts_cubit.dart | 4 +- .../local_accounts_state.dart | 0 .../user_logins_cubit/user_logins_cubit.dart | 4 +- .../user_logins_cubit/user_logins_state.dart | 0 .../account_repository.dart | 62 +- .../account_repository/active_logins.dart | 2 +- .../active_logins.freezed.dart | 0 .../account_repository/active_logins.g.dart | 0 .../encryption_key_type.dart | 43 +- .../account_repository/local_account.dart | 2 +- .../local_account.freezed.dart | 0 .../account_repository/local_account.g.dart | 0 .../account_repository/new_profile_spec.dart | 5 + .../account_repository/user_login.dart | 2 +- .../user_login.freezed.dart | 0 .../account_repository/user_login.g.dart | 0 .../repository/repository.dart | 1 + .../new_account_page/new_account_page.dart} | 53 +- lib/account_manager/view/view.dart | 1 + lib/app.dart | 60 +- lib/init.dart | 12 +- lib/main.dart | 6 +- .../components/signal_strength_meter.dart | 8 +- .../entities/preferences.g.dart | 53 -- lib/old_to_refactor/pages/developer.dart | 2 +- lib/old_to_refactor/pages/home.dart | 2 +- .../pages/main_pager/account.dart | 2 +- .../pages/main_pager/chats.dart | 2 +- .../pages/main_pager/main_pager.dart | 2 +- lib/old_to_refactor/providers/account.dart | 2 +- lib/old_to_refactor/providers/chat.dart | 2 +- .../providers/connection_state.dart | 2 +- .../providers/connection_state.freezed.dart | 28 +- lib/old_to_refactor/providers/contact.dart | 2 +- .../contact_invitation_list_manager.dart | 6 +- .../providers/contact_invite.dart | 2 +- .../providers/conversation.dart | 2 +- .../providers/window_control.dart | 83 -- lib/processor.dart | 15 +- lib/proto/proto.dart | 4 +- lib/router/cubit/router_cubit.dart | 4 +- lib/router/cubit/router_cubit.freezed.dart | 193 ----- lib/router/cubit/router_cubit.g.dart | 21 - lib/theme/theme_service.dart | 108 --- lib/tick.dart | 4 +- lib/tools/async_table_db_backed_cubit.dart | 2 +- lib/tools/loggy.dart | 2 +- lib/tools/secret_crypto.dart | 46 -- lib/tools/tools.dart | 2 +- lib/tools/window_control.dart | 65 ++ packages/veilid_support/.gitignore | 56 ++ packages/veilid_support/analysis_options.yaml | 15 + packages/veilid_support/build.bat | 7 + packages/veilid_support/build.sh | 8 + .../lib}/dht_support/dht_support.dart | 0 .../lib}/dht_support/proto/dht.proto | 0 .../lib}/dht_support/proto/proto.dart | 10 +- .../lib}/dht_support/src/dht_record.dart | 2 +- .../dht_support/src/dht_record_crypto.dart | 2 +- .../lib}/dht_support/src/dht_record_pool.dart | 2 +- .../src/dht_record_pool.freezed.dart | 0 .../dht_support/src/dht_record_pool.g.dart | 24 +- .../lib}/dht_support/src/dht_short_array.dart | 2 +- .../veilid_support/lib}/proto/dht.pb.dart | 0 .../veilid_support/lib}/proto/dht.pbenum.dart | 0 .../veilid_support/lib}/proto/dht.pbjson.dart | 0 .../lib}/proto/dht.pbserver.dart | 0 .../veilid_support/lib}/proto/proto.dart | 40 +- .../veilid_support/lib}/proto/veilid.pb.dart | 0 .../lib}/proto/veilid.pbenum.dart | 0 .../lib}/proto/veilid.pbjson.dart | 0 .../lib}/proto/veilid.pbserver.dart | 0 .../veilid_support/lib}/proto/veilid.proto | 0 .../veilid_support/lib}/src/config.dart | 10 +- .../veilid_support/lib}/src/identity.dart | 3 +- .../lib}/src/identity.freezed.dart | 0 .../veilid_support/lib}/src/identity.g.dart | 34 +- .../veilid_support/lib}/src/json_tools.dart | 0 .../lib}/src/protobuf_tools.dart | 0 .../veilid_support/lib}/src/table_db.dart | 0 .../veilid_support/lib}/src/veilid_log.dart | 5 +- .../veilid_support/lib}/veilid_support.dart | 0 packages/veilid_support/pubspec.lock | 780 ++++++++++++++++++ packages/veilid_support/pubspec.yaml | 26 + pubspec.lock | 19 +- pubspec.yaml | 2 + 91 files changed, 1237 insertions(+), 748 deletions(-) create mode 100644 lib/account_manager/account_manager.dart rename lib/{local_account_manager => account_manager/cubit}/active_user_login_cubit/active_user_login_cubit.dart (84%) rename lib/{local_account_manager => account_manager/cubit}/active_user_login_cubit/active_user_login_state.dart (100%) rename lib/{local_account_manager/local_account_manager.dart => account_manager/cubit/cubit.dart} (63%) rename lib/{local_account_manager => account_manager/cubit}/local_accounts_cubit/local_accounts_cubit.dart (89%) rename lib/{local_account_manager => account_manager/cubit}/local_accounts_cubit/local_accounts_state.dart (100%) rename lib/{local_account_manager => account_manager/cubit}/user_logins_cubit/user_logins_cubit.dart (89%) rename lib/{local_account_manager => account_manager/cubit}/user_logins_cubit/user_logins_state.dart (100%) rename lib/{local_account_manager => account_manager/repository}/account_repository/account_repository.dart (86%) rename lib/{local_account_manager => account_manager/repository}/account_repository/active_logins.dart (93%) rename lib/{local_account_manager => account_manager/repository}/account_repository/active_logins.freezed.dart (100%) rename lib/{local_account_manager => account_manager/repository}/account_repository/active_logins.g.dart (100%) rename lib/{local_account_manager => account_manager/repository}/account_repository/encryption_key_type.dart (52%) rename lib/{local_account_manager => account_manager/repository}/account_repository/local_account.dart (96%) rename lib/{local_account_manager => account_manager/repository}/account_repository/local_account.freezed.dart (100%) rename lib/{local_account_manager => account_manager/repository}/account_repository/local_account.g.dart (100%) create mode 100644 lib/account_manager/repository/account_repository/new_profile_spec.dart rename lib/{local_account_manager => account_manager/repository}/account_repository/user_login.dart (94%) rename lib/{local_account_manager => account_manager/repository}/account_repository/user_login.freezed.dart (100%) rename lib/{local_account_manager => account_manager/repository}/account_repository/user_login.g.dart (100%) create mode 100644 lib/account_manager/repository/repository.dart rename lib/{old_to_refactor/pages/new_account.dart => account_manager/view/new_account_page/new_account_page.dart} (70%) create mode 100644 lib/account_manager/view/view.dart delete mode 100644 lib/old_to_refactor/entities/preferences.g.dart delete mode 100644 lib/old_to_refactor/providers/window_control.dart delete mode 100644 lib/router/cubit/router_cubit.freezed.dart delete mode 100644 lib/router/cubit/router_cubit.g.dart delete mode 100644 lib/theme/theme_service.dart delete mode 100644 lib/tools/secret_crypto.dart create mode 100644 lib/tools/window_control.dart create mode 100644 packages/veilid_support/.gitignore create mode 100644 packages/veilid_support/analysis_options.yaml create mode 100644 packages/veilid_support/build.bat create mode 100755 packages/veilid_support/build.sh rename {lib/veilid_support => packages/veilid_support/lib}/dht_support/dht_support.dart (100%) rename {lib/veilid_support => packages/veilid_support/lib}/dht_support/proto/dht.proto (100%) rename {lib/veilid_support => packages/veilid_support/lib}/dht_support/proto/proto.dart (75%) rename {lib/veilid_support => packages/veilid_support/lib}/dht_support/src/dht_record.dart (99%) rename {lib/veilid_support => packages/veilid_support/lib}/dht_support/src/dht_record_crypto.dart (97%) rename {lib/veilid_support => packages/veilid_support/lib}/dht_support/src/dht_record_pool.dart (99%) rename {lib/veilid_support => packages/veilid_support/lib}/dht_support/src/dht_record_pool.freezed.dart (100%) rename {lib/veilid_support => packages/veilid_support/lib}/dht_support/src/dht_record_pool.g.dart (75%) rename {lib/veilid_support => packages/veilid_support/lib}/dht_support/src/dht_short_array.dart (99%) rename {lib => packages/veilid_support/lib}/proto/dht.pb.dart (100%) rename {lib => packages/veilid_support/lib}/proto/dht.pbenum.dart (100%) rename {lib => packages/veilid_support/lib}/proto/dht.pbjson.dart (100%) rename {lib => packages/veilid_support/lib}/proto/dht.pbserver.dart (100%) rename {lib/veilid_support => packages/veilid_support/lib}/proto/proto.dart (75%) rename {lib => packages/veilid_support/lib}/proto/veilid.pb.dart (100%) rename {lib => packages/veilid_support/lib}/proto/veilid.pbenum.dart (100%) rename {lib => packages/veilid_support/lib}/proto/veilid.pbjson.dart (100%) rename {lib => packages/veilid_support/lib}/proto/veilid.pbserver.dart (100%) rename {lib/veilid_support => packages/veilid_support/lib}/proto/veilid.proto (100%) rename {lib/veilid_support => packages/veilid_support/lib}/src/config.dart (93%) rename {lib/veilid_support => packages/veilid_support/lib}/src/identity.dart (99%) rename {lib/veilid_support => packages/veilid_support/lib}/src/identity.freezed.dart (100%) rename {lib/veilid_support => packages/veilid_support/lib}/src/identity.g.dart (62%) rename {lib/veilid_support => packages/veilid_support/lib}/src/json_tools.dart (100%) rename {lib/veilid_support => packages/veilid_support/lib}/src/protobuf_tools.dart (100%) rename {lib/veilid_support => packages/veilid_support/lib}/src/table_db.dart (100%) rename {lib/veilid_support => packages/veilid_support/lib}/src/veilid_log.dart (94%) rename {lib/veilid_support => packages/veilid_support/lib}/veilid_support.dart (100%) create mode 100644 packages/veilid_support/pubspec.lock create mode 100644 packages/veilid_support/pubspec.yaml diff --git a/build.sh b/build.sh index 96cce52..1a6cdf5 100755 --- a/build.sh +++ b/build.sh @@ -3,7 +3,5 @@ set -e dart run build_runner build --delete-conflicting-outputs pushd lib > /dev/null -protoc --dart_out=proto -I veilid_support/proto -I veilid_support/dht_support/proto -I proto veilidchat.proto -protoc --dart_out=proto -I veilid_support/proto -I veilid_support/dht_support/proto dht.proto -protoc --dart_out=proto -I veilid_support/proto veilid.proto +protoc --dart_out=proto -I ../packages/veilid_support/lib/proto -I ../packages/veilid_support/lib/dht_support/proto -I proto veilidchat.proto popd > /dev/null diff --git a/lib/account_manager/account_manager.dart b/lib/account_manager/account_manager.dart new file mode 100644 index 0000000..d04e6f0 --- /dev/null +++ b/lib/account_manager/account_manager.dart @@ -0,0 +1,3 @@ +export 'cubit/cubit.dart'; +export 'repository/repository.dart'; +export 'view/view.dart'; diff --git a/lib/local_account_manager/active_user_login_cubit/active_user_login_cubit.dart b/lib/account_manager/cubit/active_user_login_cubit/active_user_login_cubit.dart similarity index 84% rename from lib/local_account_manager/active_user_login_cubit/active_user_login_cubit.dart rename to lib/account_manager/cubit/active_user_login_cubit/active_user_login_cubit.dart index b8d4dcf..ec0aaf5 100644 --- a/lib/local_account_manager/active_user_login_cubit/active_user_login_cubit.dart +++ b/lib/account_manager/cubit/active_user_login_cubit/active_user_login_cubit.dart @@ -1,14 +1,14 @@ import 'dart:async'; import 'package:bloc/bloc.dart'; +import 'package:veilid_support/veilid_support.dart'; -import '../../veilid_support/veilid_support.dart'; -import '../account_repository/account_repository.dart'; +import '../../repository/account_repository/account_repository.dart'; part 'active_user_login_state.dart'; class ActiveUserLoginCubit extends Cubit { - ActiveUserLoginCubit({required AccountRepository accountRepository}) + ActiveUserLoginCubit(AccountRepository accountRepository) : _accountRepository = accountRepository, super(null) { // Subscribe to streams diff --git a/lib/local_account_manager/active_user_login_cubit/active_user_login_state.dart b/lib/account_manager/cubit/active_user_login_cubit/active_user_login_state.dart similarity index 100% rename from lib/local_account_manager/active_user_login_cubit/active_user_login_state.dart rename to lib/account_manager/cubit/active_user_login_cubit/active_user_login_state.dart diff --git a/lib/local_account_manager/local_account_manager.dart b/lib/account_manager/cubit/cubit.dart similarity index 63% rename from lib/local_account_manager/local_account_manager.dart rename to lib/account_manager/cubit/cubit.dart index 2b0a87f..0f56c84 100644 --- a/lib/local_account_manager/local_account_manager.dart +++ b/lib/account_manager/cubit/cubit.dart @@ -1,3 +1,3 @@ -export 'account_repository/account_repository.dart'; +export 'active_user_login_cubit/active_user_login_cubit.dart'; export 'local_accounts_cubit/local_accounts_cubit.dart'; export 'user_logins_cubit/user_logins_cubit.dart'; diff --git a/lib/local_account_manager/local_accounts_cubit/local_accounts_cubit.dart b/lib/account_manager/cubit/local_accounts_cubit/local_accounts_cubit.dart similarity index 89% rename from lib/local_account_manager/local_accounts_cubit/local_accounts_cubit.dart rename to lib/account_manager/cubit/local_accounts_cubit/local_accounts_cubit.dart index aa54d6c..34fdccb 100644 --- a/lib/local_account_manager/local_accounts_cubit/local_accounts_cubit.dart +++ b/lib/account_manager/cubit/local_accounts_cubit/local_accounts_cubit.dart @@ -3,12 +3,12 @@ import 'dart:async'; import 'package:bloc/bloc.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; -import '../account_repository/account_repository.dart'; +import '../../repository/account_repository/account_repository.dart'; part 'local_accounts_state.dart'; class LocalAccountsCubit extends Cubit { - LocalAccountsCubit({required AccountRepository accountRepository}) + LocalAccountsCubit(AccountRepository accountRepository) : _accountRepository = accountRepository, super(LocalAccountsState()) { // Subscribe to streams diff --git a/lib/local_account_manager/local_accounts_cubit/local_accounts_state.dart b/lib/account_manager/cubit/local_accounts_cubit/local_accounts_state.dart similarity index 100% rename from lib/local_account_manager/local_accounts_cubit/local_accounts_state.dart rename to lib/account_manager/cubit/local_accounts_cubit/local_accounts_state.dart diff --git a/lib/local_account_manager/user_logins_cubit/user_logins_cubit.dart b/lib/account_manager/cubit/user_logins_cubit/user_logins_cubit.dart similarity index 89% rename from lib/local_account_manager/user_logins_cubit/user_logins_cubit.dart rename to lib/account_manager/cubit/user_logins_cubit/user_logins_cubit.dart index e8d9bef..e6ad92a 100644 --- a/lib/local_account_manager/user_logins_cubit/user_logins_cubit.dart +++ b/lib/account_manager/cubit/user_logins_cubit/user_logins_cubit.dart @@ -3,12 +3,12 @@ import 'dart:async'; import 'package:bloc/bloc.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; -import '../account_repository/account_repository.dart'; +import '../../repository/account_repository/account_repository.dart'; part 'user_logins_state.dart'; class UserLoginsCubit extends Cubit { - UserLoginsCubit({required AccountRepository accountRepository}) + UserLoginsCubit(AccountRepository accountRepository) : _accountRepository = accountRepository, super(UserLoginsState()) { // Subscribe to streams diff --git a/lib/local_account_manager/user_logins_cubit/user_logins_state.dart b/lib/account_manager/cubit/user_logins_cubit/user_logins_state.dart similarity index 100% rename from lib/local_account_manager/user_logins_cubit/user_logins_state.dart rename to lib/account_manager/cubit/user_logins_cubit/user_logins_state.dart diff --git a/lib/local_account_manager/account_repository/account_repository.dart b/lib/account_manager/repository/account_repository/account_repository.dart similarity index 86% rename from lib/local_account_manager/account_repository/account_repository.dart rename to lib/account_manager/repository/account_repository/account_repository.dart index 04be9e5..0e42a55 100644 --- a/lib/local_account_manager/account_repository/account_repository.dart +++ b/lib/account_manager/repository/account_repository/account_repository.dart @@ -1,11 +1,11 @@ import 'package:fast_immutable_collections/fast_immutable_collections.dart'; +import 'package:veilid_support/veilid_support.dart'; -import '../../../proto/proto.dart' as proto; -import '../../tools/tools.dart'; -import '../../veilid_support/veilid_support.dart'; +import '../../../../proto/proto.dart' as proto; import 'active_logins.dart'; import 'encryption_key_type.dart'; import 'local_account.dart'; +import 'new_profile_spec.dart'; import 'user_login.dart'; export 'active_logins.dart'; @@ -45,15 +45,7 @@ class AccountRepository { ////////////////////////////////////////////////////////////// /// Singleton initialization - static AccountRepository? _instance; - static Future get instance async { - if (_instance == null) { - final accountRepository = AccountRepository._(); - await accountRepository.init(); - _instance = accountRepository; - } - return _instance!; - } + static AccountRepository instance = AccountRepository._(); Future init() async { await _localAccounts.load(); @@ -104,13 +96,33 @@ class AccountRepository { await _localAccounts.set(updated); } + /// Creates a new master identity, an account associated with the master + /// identity, stores the account in the identity key and then logs into + /// that account with no password set at this time + Future createMasterIdentity(NewProfileSpec newProfileSpec) async { + final imws = await IdentityMasterWithSecrets.create(); + try { + final localAccount = await _newLocalAccount( + identityMaster: imws.identityMaster, + identitySecret: imws.identitySecret, + newProfileSpec: newProfileSpec); + + // Log in the new account by default with no pin + final ok = await login(localAccount.identityMaster.masterRecordKey, + EncryptionKeyType.none, ''); + assert(ok, 'login with none should never fail'); + } on Exception catch (_) { + await imws.delete(); + rethrow; + } + } + /// Creates a new Account associated with master identity /// Adds a logged-out LocalAccount to track its existence on this device - Future newLocalAccount( + Future _newLocalAccount( {required IdentityMaster identityMaster, required SecretKey identitySecret, - required String name, - required String pronouns, + required NewProfileSpec newProfileSpec, EncryptionKeyType encryptionKeyType = EncryptionKeyType.none, String encryptionKey = ''}) async { final localAccounts = await _localAccounts.get(); @@ -136,8 +148,8 @@ class AccountRepository { // Make account object final account = proto.Account() ..profile = (proto.Profile() - ..name = name - ..pronouns = pronouns) + ..name = newProfileSpec.name + ..pronouns = newProfileSpec.pronouns) ..contactList = contactList.toProto() ..contactInvitationRecords = contactInvitationRecords.toProto() ..chatList = chatRecords.toProto(); @@ -145,11 +157,11 @@ class AccountRepository { }); // Encrypt identitySecret with key - final identitySecretBytes = await encryptSecretToBytes( - secret: identitySecret, - cryptoKind: identityMaster.identityRecordKey.kind, - encryptionKey: encryptionKey, - encryptionKeyType: encryptionKeyType); + final identitySecretBytes = await encryptionKeyType.encryptSecretToBytes( + secret: identitySecret, + cryptoKind: identityMaster.identityRecordKey.kind, + encryptionKey: encryptionKey, + ); // Create local account object // Does not contain the account key or its secret @@ -161,7 +173,7 @@ class AccountRepository { encryptionKeyType: encryptionKeyType, biometricsEnabled: false, hiddenAccount: false, - name: name, + name: newProfileSpec.name, ); // Add local account object to internal store @@ -257,10 +269,10 @@ class AccountRepository { throw Exception('Wrong authentication type'); } - final identitySecret = await decryptSecretFromBytes( + final identitySecret = + await localAccount.encryptionKeyType.decryptSecretFromBytes( secretBytes: localAccount.identitySecretBytes, cryptoKind: localAccount.identityMaster.identityRecordKey.kind, - encryptionKeyType: localAccount.encryptionKeyType, encryptionKey: encryptionKey, ); diff --git a/lib/local_account_manager/account_repository/active_logins.dart b/lib/account_manager/repository/account_repository/active_logins.dart similarity index 93% rename from lib/local_account_manager/account_repository/active_logins.dart rename to lib/account_manager/repository/account_repository/active_logins.dart index fd83e37..2fab41f 100644 --- a/lib/local_account_manager/account_repository/active_logins.dart +++ b/lib/account_manager/repository/account_repository/active_logins.dart @@ -1,8 +1,8 @@ // Represents a set of user logins and the currently selected account import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:veilid_support/veilid_support.dart'; -import '../../veilid_support/veilid_support.dart'; import 'user_login.dart'; part 'active_logins.g.dart'; diff --git a/lib/local_account_manager/account_repository/active_logins.freezed.dart b/lib/account_manager/repository/account_repository/active_logins.freezed.dart similarity index 100% rename from lib/local_account_manager/account_repository/active_logins.freezed.dart rename to lib/account_manager/repository/account_repository/active_logins.freezed.dart diff --git a/lib/local_account_manager/account_repository/active_logins.g.dart b/lib/account_manager/repository/account_repository/active_logins.g.dart similarity index 100% rename from lib/local_account_manager/account_repository/active_logins.g.dart rename to lib/account_manager/repository/account_repository/active_logins.g.dart diff --git a/lib/local_account_manager/account_repository/encryption_key_type.dart b/lib/account_manager/repository/account_repository/encryption_key_type.dart similarity index 52% rename from lib/local_account_manager/account_repository/encryption_key_type.dart rename to lib/account_manager/repository/account_repository/encryption_key_type.dart index 45fd682..2c00f27 100644 --- a/lib/local_account_manager/account_repository/encryption_key_type.dart +++ b/lib/account_manager/repository/account_repository/encryption_key_type.dart @@ -4,9 +4,12 @@ // * Pin : Code is a numeric pin (4-256 numeric digits) hashed with Argon2 // * Password: Code is a UTF-8 string that is hashed with Argon2 -import 'package:change_case/change_case.dart'; +import 'dart:typed_data'; -import '../../../proto/proto.dart' as proto; +import 'package:change_case/change_case.dart'; +import 'package:veilid_support/veilid_support.dart'; + +import '../../../../proto/proto.dart' as proto; enum EncryptionKeyType { none, @@ -37,4 +40,40 @@ enum EncryptionKeyType { EncryptionKeyType.password => proto.EncryptionKeyType.ENCRYPTION_KEY_TYPE_PASSWORD, }; + + Future encryptSecretToBytes( + {required SecretKey secret, + required CryptoKind cryptoKind, + String encryptionKey = ''}) async { + late final Uint8List secretBytes; + switch (this) { + case EncryptionKeyType.none: + secretBytes = secret.decode(); + case EncryptionKeyType.pin: + case EncryptionKeyType.password: + final cs = await Veilid.instance.getCryptoSystem(cryptoKind); + + secretBytes = + await cs.encryptAeadWithPassword(secret.decode(), encryptionKey); + } + return secretBytes; + } + + Future decryptSecretFromBytes( + {required Uint8List secretBytes, + required CryptoKind cryptoKind, + String encryptionKey = ''}) async { + late final SecretKey secret; + switch (this) { + case EncryptionKeyType.none: + secret = SecretKey.fromBytes(secretBytes); + case EncryptionKeyType.pin: + case EncryptionKeyType.password: + final cs = await Veilid.instance.getCryptoSystem(cryptoKind); + + secret = SecretKey.fromBytes( + await cs.decryptAeadWithPassword(secretBytes, encryptionKey)); + } + return secret; + } } diff --git a/lib/local_account_manager/account_repository/local_account.dart b/lib/account_manager/repository/account_repository/local_account.dart similarity index 96% rename from lib/local_account_manager/account_repository/local_account.dart rename to lib/account_manager/repository/account_repository/local_account.dart index 6aa2c4a..eaf0fa8 100644 --- a/lib/local_account_manager/account_repository/local_account.dart +++ b/lib/account_manager/repository/account_repository/local_account.dart @@ -1,8 +1,8 @@ import 'dart:typed_data'; import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:veilid_support/veilid_support.dart'; -import '../../veilid_support/veilid_support.dart'; import 'encryption_key_type.dart'; part 'local_account.g.dart'; diff --git a/lib/local_account_manager/account_repository/local_account.freezed.dart b/lib/account_manager/repository/account_repository/local_account.freezed.dart similarity index 100% rename from lib/local_account_manager/account_repository/local_account.freezed.dart rename to lib/account_manager/repository/account_repository/local_account.freezed.dart diff --git a/lib/local_account_manager/account_repository/local_account.g.dart b/lib/account_manager/repository/account_repository/local_account.g.dart similarity index 100% rename from lib/local_account_manager/account_repository/local_account.g.dart rename to lib/account_manager/repository/account_repository/local_account.g.dart diff --git a/lib/account_manager/repository/account_repository/new_profile_spec.dart b/lib/account_manager/repository/account_repository/new_profile_spec.dart new file mode 100644 index 0000000..173a382 --- /dev/null +++ b/lib/account_manager/repository/account_repository/new_profile_spec.dart @@ -0,0 +1,5 @@ +class NewProfileSpec { + NewProfileSpec({required this.name, required this.pronouns}); + String name; + String pronouns; +} diff --git a/lib/local_account_manager/account_repository/user_login.dart b/lib/account_manager/repository/account_repository/user_login.dart similarity index 94% rename from lib/local_account_manager/account_repository/user_login.dart rename to lib/account_manager/repository/account_repository/user_login.dart index 2708ce4..4e23184 100644 --- a/lib/local_account_manager/account_repository/user_login.dart +++ b/lib/account_manager/repository/account_repository/user_login.dart @@ -1,6 +1,6 @@ import 'package:freezed_annotation/freezed_annotation.dart'; -import '../../veilid_support/veilid_support.dart'; +import 'package:veilid_support/veilid_support.dart'; part 'user_login.freezed.dart'; part 'user_login.g.dart'; diff --git a/lib/local_account_manager/account_repository/user_login.freezed.dart b/lib/account_manager/repository/account_repository/user_login.freezed.dart similarity index 100% rename from lib/local_account_manager/account_repository/user_login.freezed.dart rename to lib/account_manager/repository/account_repository/user_login.freezed.dart diff --git a/lib/local_account_manager/account_repository/user_login.g.dart b/lib/account_manager/repository/account_repository/user_login.g.dart similarity index 100% rename from lib/local_account_manager/account_repository/user_login.g.dart rename to lib/account_manager/repository/account_repository/user_login.g.dart diff --git a/lib/account_manager/repository/repository.dart b/lib/account_manager/repository/repository.dart new file mode 100644 index 0000000..9d1b9fe --- /dev/null +++ b/lib/account_manager/repository/repository.dart @@ -0,0 +1 @@ +export 'account_repository/account_repository.dart'; diff --git a/lib/old_to_refactor/pages/new_account.dart b/lib/account_manager/view/new_account_page/new_account_page.dart similarity index 70% rename from lib/old_to_refactor/pages/new_account.dart rename to lib/account_manager/view/new_account_page/new_account_page.dart index 18d3963..acf3f8d 100644 --- a/lib/old_to_refactor/pages/new_account.dart +++ b/lib/account_manager/view/new_account_page/new_account_page.dart @@ -2,19 +2,15 @@ import 'package:awesome_extensions/awesome_extensions.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_form_builder/flutter_form_builder.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_translate/flutter_translate.dart'; import 'package:form_builder_validators/form_builder_validators.dart'; import 'package:go_router/go_router.dart'; +import 'package:veilid_support/veilid_support.dart'; -import '../../components/default_app_bar.dart'; -import '../../components/signal_strength_meter.dart'; -import '../../entities/entities.dart'; -import '../../local_accounts/local_accounts.dart'; -import '../providers/logins.dart'; -import '../providers/window_control.dart'; -import '../../tools/tools.dart'; -import '../../veilid_support/veilid_support.dart'; +import '../../../components/default_app_bar.dart'; +import '../../../components/signal_strength_meter.dart'; +import '../../../entities/entities.dart'; +import '../../../tools/tools.dart'; class NewAccountPage extends StatefulWidget { const NewAccountPage({super.key}); @@ -23,7 +19,7 @@ class NewAccountPage extends StatefulWidget { NewAccountPageState createState() => NewAccountPageState(); } -class NewAccountPageState extends ConsumerState { +class NewAccountPageState extends State { final _formKey = GlobalKey(); late bool isInAsyncCall = false; static const String formFieldName = 'name'; @@ -35,41 +31,11 @@ class NewAccountPageState extends ConsumerState { WidgetsBinding.instance.addPostFrameCallback((_) async { setState(() {}); - await ref.read(windowControlProvider.notifier).changeWindowSetup( + await changeWindowSetup( TitleBarStyle.normal, OrientationCapability.portraitOnly); }); } - /// Creates a new master identity, an account associated with the master - /// identity, stores the account in the identity key and then logs into - /// that account with no password set at this time - Future createAccount() async { - final localAccounts = ref.read(localAccountsProvider.notifier); - final logins = ref.read(loginsProvider.notifier); - - final name = _formKey.currentState!.fields[formFieldName]!.value as String; - final pronouns = - _formKey.currentState!.fields[formFieldPronouns]!.value as String? ?? - ''; - - final imws = await IdentityMasterWithSecrets.create(); - try { - final localAccount = await localAccounts.newLocalAccount( - identityMaster: imws.identityMaster, - identitySecret: imws.identitySecret, - name: name, - pronouns: pronouns); - - // Log in the new account by default with no pin - final ok = await logins.login(localAccount.identityMaster.masterRecordKey, - EncryptionKeyType.none, ''); - assert(ok, 'login with none should never fail'); - } on Exception catch (_) { - await imws.delete(); - rethrow; - } - } - Widget _newAccountForm(BuildContext context, {required Future Function(GlobalKey) onSubmit}) => @@ -129,11 +95,6 @@ class NewAccountPageState extends ConsumerState { @override Widget build(BuildContext context) { - ref.watch(windowControlProvider); - - final localAccounts = ref.watch(localAccountsProvider); - final logins = ref.watch(loginsProvider); - final displayModalHUD = isInAsyncCall || !localAccounts.hasValue || !logins.hasValue; diff --git a/lib/account_manager/view/view.dart b/lib/account_manager/view/view.dart new file mode 100644 index 0000000..3304b76 --- /dev/null +++ b/lib/account_manager/view/view.dart @@ -0,0 +1 @@ +export 'new_account_page/new_account_page.dart'; diff --git a/lib/app.dart b/lib/app.dart index 2510f69..903d4f9 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -6,6 +6,7 @@ import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:flutter_translate/flutter_translate.dart'; import 'package:form_builder_validators/form_builder_validators.dart'; +import 'account_manager/account_manager.dart'; import 'router/router.dart'; import 'tick.dart'; @@ -15,6 +16,8 @@ class VeilidChatApp extends StatelessWidget { super.key, }); + static const String name = 'VeilidChat'; + final ThemeData themeData; @override @@ -25,27 +28,42 @@ class VeilidChatApp extends StatelessWidget { initTheme: themeData, builder: (_, theme) => LocalizationProvider( state: LocalizationProvider.of(context).state, - child: BackgroundTicker( - builder: (context) => BlocProvider( - create: (context) => RouterCubit(), - child: MaterialApp.router( - debugShowCheckedModeBanner: false, - routerConfig: router( - routerCubit: - BlocProvider.of(context)), - title: translate('app.title'), - theme: theme, - localizationsDelegates: [ - GlobalMaterialLocalizations.delegate, - GlobalWidgetsLocalizations.delegate, - FormBuilderLocalizations.delegate, - localizationDelegate - ], - supportedLocales: - localizationDelegate.supportedLocales, - locale: localizationDelegate.currentLocale, - ), - )), + child: MultiBlocProvider( + providers: [ + BlocProvider( + create: (context) => + RouterCubit(AccountRepository.instance), + ), + BlocProvider( + create: (context) => + LocalAccountsCubit(AccountRepository.instance), + ), + BlocProvider( + create: (context) => + UserLoginsCubit(AccountRepository.instance), + ), + BlocProvider( + create: (context) => + ActiveUserLoginCubit(AccountRepository.instance), + ), + ], + child: BackgroundTicker( + builder: (context) => MaterialApp.router( + debugShowCheckedModeBanner: false, + routerConfig: router( + routerCubit: BlocProvider.of(context)), + title: translate('app.title'), + theme: theme, + localizationsDelegates: [ + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + FormBuilderLocalizations.delegate, + localizationDelegate + ], + supportedLocales: localizationDelegate.supportedLocales, + locale: localizationDelegate.currentLocale, + ), + )), )); } diff --git a/lib/init.dart b/lib/init.dart index 3305e33..7c0ac92 100644 --- a/lib/init.dart +++ b/lib/init.dart @@ -1,11 +1,10 @@ import 'dart:async'; -import 'local_account_manager/local_account_manager.dart'; +import 'app.dart'; +import 'local_account_manager/account_manager.dart'; import 'processor.dart'; import 'tools/tools.dart'; -import 'veilid_support/veilid_support.dart'; - -const String appName = 'VeilidChat'; +import '../packages/veilid_support/veilid_support.dart'; final Completer eventualVeilid = Completer(); final Processor processor = Processor(); @@ -20,7 +19,8 @@ Future initializeVeilid() async { } // Init Veilid - Veilid.instance.initializeVeilidCore(getDefaultVeilidPlatformConfig(appName)); + Veilid.instance + .initializeVeilidCore(getDefaultVeilidPlatformConfig(VeilidChatApp.name)); // Veilid logging initVeilidLog(); @@ -34,7 +34,7 @@ Future initializeVeilid() async { // Initialize repositories Future initializeRepositories() async { - await AccountRepository.instance; + await AccountRepository.instance.init(); } Future initializeVeilidChat() async { diff --git a/lib/main.dart b/lib/main.dart index 522bd00..3a44cc9 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -8,11 +8,9 @@ import 'package:flutter_translate/flutter_translate.dart'; import 'package:intl/date_symbol_data_local.dart'; import 'app.dart'; -import 'old_to_refactor/providers/window_control.dart'; +import 'init.dart'; import 'theme/theme.dart'; import 'tools/tools.dart'; -import 'init.dart'; - void main() async { // Disable all debugprints in release mode @@ -39,7 +37,7 @@ void main() async { final themeData = themeRepository.themeData(); // Manage window on desktop platforms - await WindowControl.initialize(); + await initializeWindowControl(); // Make localization delegate final delegate = await LocalizationDelegate.create( diff --git a/lib/old_to_refactor/components/signal_strength_meter.dart b/lib/old_to_refactor/components/signal_strength_meter.dart index c093529..2593515 100644 --- a/lib/old_to_refactor/components/signal_strength_meter.dart +++ b/lib/old_to_refactor/components/signal_strength_meter.dart @@ -1,13 +1,13 @@ import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:signal_strength_indicator/signal_strength_indicator.dart'; -import 'package:go_router/go_router.dart'; +import 'package:veilid_support/veilid_support.dart'; import '../providers/connection_state.dart'; import '../tools/tools.dart'; -import '../veilid_support/veilid_support.dart'; -class SignalStrengthMeterWidget extends ConsumerWidget { +xxx move to feature level + +class SignalStrengthMeterWidget extends Widget { const SignalStrengthMeterWidget({super.key}); @override diff --git a/lib/old_to_refactor/entities/preferences.g.dart b/lib/old_to_refactor/entities/preferences.g.dart deleted file mode 100644 index 0e6f96c..0000000 --- a/lib/old_to_refactor/entities/preferences.g.dart +++ /dev/null @@ -1,53 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'preferences.dart'; - -// ************************************************************************** -// JsonSerializableGenerator -// ************************************************************************** - -_$LockPreferenceImpl _$$LockPreferenceImplFromJson(Map json) => - _$LockPreferenceImpl( - inactivityLockSecs: json['inactivity_lock_secs'] as int, - lockWhenSwitching: json['lock_when_switching'] as bool, - lockWithSystemLock: json['lock_with_system_lock'] as bool, - ); - -Map _$$LockPreferenceImplToJson( - _$LockPreferenceImpl instance) => - { - 'inactivity_lock_secs': instance.inactivityLockSecs, - 'lock_when_switching': instance.lockWhenSwitching, - 'lock_with_system_lock': instance.lockWithSystemLock, - }; - -_$ThemePreferencesImpl _$$ThemePreferencesImplFromJson( - Map json) => - _$ThemePreferencesImpl( - brightnessPreference: - BrightnessPreference.fromJson(json['brightness_preference']), - colorPreference: ColorPreference.fromJson(json['color_preference']), - displayScale: (json['display_scale'] as num).toDouble(), - ); - -Map _$$ThemePreferencesImplToJson( - _$ThemePreferencesImpl instance) => - { - 'brightness_preference': instance.brightnessPreference.toJson(), - 'color_preference': instance.colorPreference.toJson(), - 'display_scale': instance.displayScale, - }; - -_$PreferencesImpl _$$PreferencesImplFromJson(Map json) => - _$PreferencesImpl( - themePreferences: ThemePreferences.fromJson(json['theme_preferences']), - language: LanguagePreference.fromJson(json['language']), - locking: LockPreference.fromJson(json['locking']), - ); - -Map _$$PreferencesImplToJson(_$PreferencesImpl instance) => - { - 'theme_preferences': instance.themePreferences.toJson(), - 'language': instance.language.toJson(), - 'locking': instance.locking.toJson(), - }; diff --git a/lib/old_to_refactor/pages/developer.dart b/lib/old_to_refactor/pages/developer.dart index 6a7bd91..1dc56cd 100644 --- a/lib/old_to_refactor/pages/developer.dart +++ b/lib/old_to_refactor/pages/developer.dart @@ -14,7 +14,7 @@ import 'package:quickalert/quickalert.dart'; import 'package:xterm/xterm.dart'; import '../../tools/tools.dart'; -import '../../veilid_support/veilid_support.dart'; +import '../../../packages/veilid_support/veilid_support.dart'; final globalDebugTerminal = Terminal( maxLines: 50000, diff --git a/lib/old_to_refactor/pages/home.dart b/lib/old_to_refactor/pages/home.dart index 78b09aa..6284dfd 100644 --- a/lib/old_to_refactor/pages/home.dart +++ b/lib/old_to_refactor/pages/home.dart @@ -18,7 +18,7 @@ import '../../local_accounts/local_accounts.dart'; import '../providers/logins.dart'; import '../providers/window_control.dart'; import '../../tools/tools.dart'; -import '../../veilid_support/veilid_support.dart'; +import '../../../packages/veilid_support/veilid_support.dart'; import 'main_pager/main_pager.dart'; class HomePage extends StatefulWidget { diff --git a/lib/old_to_refactor/pages/main_pager/account.dart b/lib/old_to_refactor/pages/main_pager/account.dart index ca6aa1d..553c288 100644 --- a/lib/old_to_refactor/pages/main_pager/account.dart +++ b/lib/old_to_refactor/pages/main_pager/account.dart @@ -15,7 +15,7 @@ import '../../providers/contact.dart'; import '../../providers/contact_invite.dart'; import '../../../theme/theme.dart'; import '../../../tools/tools.dart'; -import '../../../veilid_support/veilid_support.dart'; +import '../../../../packages/veilid_support/veilid_support.dart'; class AccountPage extends ConsumerStatefulWidget { const AccountPage({ diff --git a/lib/old_to_refactor/pages/main_pager/chats.dart b/lib/old_to_refactor/pages/main_pager/chats.dart index c98d128..d35fe6b 100644 --- a/lib/old_to_refactor/pages/main_pager/chats.dart +++ b/lib/old_to_refactor/pages/main_pager/chats.dart @@ -13,7 +13,7 @@ import '../../providers/contact.dart'; import '../../../local_accounts/local_accounts.dart'; import '../../providers/logins.dart'; import '../../../tools/tools.dart'; -import '../../../veilid_support/veilid_support.dart'; +import '../../../../packages/veilid_support/veilid_support.dart'; class ChatsPage extends ConsumerStatefulWidget { const ChatsPage({super.key}); diff --git a/lib/old_to_refactor/pages/main_pager/main_pager.dart b/lib/old_to_refactor/pages/main_pager/main_pager.dart index fd3ef14..971ec1f 100644 --- a/lib/old_to_refactor/pages/main_pager/main_pager.dart +++ b/lib/old_to_refactor/pages/main_pager/main_pager.dart @@ -20,7 +20,7 @@ import '../../../components/send_invite_dialog.dart'; import '../../../entities/local_account.dart'; import '../../../proto/proto.dart' as proto; import '../../../tools/tools.dart'; -import '../../../veilid_support/veilid_support.dart'; +import '../../../../packages/veilid_support/veilid_support.dart'; import 'account.dart'; import 'chats.dart'; diff --git a/lib/old_to_refactor/providers/account.dart b/lib/old_to_refactor/providers/account.dart index 8da8d58..22bdca1 100644 --- a/lib/old_to_refactor/providers/account.dart +++ b/lib/old_to_refactor/providers/account.dart @@ -4,7 +4,7 @@ import 'package:riverpod_annotation/riverpod_annotation.dart'; import '../../entities/local_account.dart'; import '../../entities/user_login.dart'; import '../../proto/proto.dart' as proto; -import '../../veilid_support/veilid_support.dart'; +import '../../../packages/veilid_support/veilid_support.dart'; import '../../local_accounts/local_accounts.dart'; import 'logins.dart'; diff --git a/lib/old_to_refactor/providers/chat.dart b/lib/old_to_refactor/providers/chat.dart index ca1c086..fa4a011 100644 --- a/lib/old_to_refactor/providers/chat.dart +++ b/lib/old_to_refactor/providers/chat.dart @@ -4,7 +4,7 @@ import 'package:riverpod_annotation/riverpod_annotation.dart'; import '../../proto/proto.dart' as proto; -import '../../veilid_support/veilid_support.dart'; +import '../../../packages/veilid_support/veilid_support.dart'; import 'account.dart'; part 'chat.g.dart'; diff --git a/lib/old_to_refactor/providers/connection_state.dart b/lib/old_to_refactor/providers/connection_state.dart index 1adf8f7..a663190 100644 --- a/lib/old_to_refactor/providers/connection_state.dart +++ b/lib/old_to_refactor/providers/connection_state.dart @@ -1,7 +1,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; -import '../../veilid_support/veilid_support.dart'; +import '../../../packages/veilid_support/veilid_support.dart'; part 'connection_state.freezed.dart'; diff --git a/lib/old_to_refactor/providers/connection_state.freezed.dart b/lib/old_to_refactor/providers/connection_state.freezed.dart index 9616832..8ac0282 100644 --- a/lib/old_to_refactor/providers/connection_state.freezed.dart +++ b/lib/old_to_refactor/providers/connection_state.freezed.dart @@ -30,8 +30,6 @@ abstract class $ConnectionStateCopyWith<$Res> { _$ConnectionStateCopyWithImpl<$Res, ConnectionState>; @useResult $Res call({VeilidStateAttachment attachment}); - - $VeilidStateAttachmentCopyWith<$Res> get attachment; } /// @nodoc @@ -47,23 +45,15 @@ class _$ConnectionStateCopyWithImpl<$Res, $Val extends ConnectionState> @pragma('vm:prefer-inline') @override $Res call({ - Object? attachment = null, + Object? attachment = freezed, }) { return _then(_value.copyWith( - attachment: null == attachment + attachment: freezed == attachment ? _value.attachment : attachment // ignore: cast_nullable_to_non_nullable as VeilidStateAttachment, ) as $Val); } - - @override - @pragma('vm:prefer-inline') - $VeilidStateAttachmentCopyWith<$Res> get attachment { - return $VeilidStateAttachmentCopyWith<$Res>(_value.attachment, (value) { - return _then(_value.copyWith(attachment: value) as $Val); - }); - } } /// @nodoc @@ -75,9 +65,6 @@ abstract class _$$ConnectionStateImplCopyWith<$Res> @override @useResult $Res call({VeilidStateAttachment attachment}); - - @override - $VeilidStateAttachmentCopyWith<$Res> get attachment; } /// @nodoc @@ -91,10 +78,10 @@ class __$$ConnectionStateImplCopyWithImpl<$Res> @pragma('vm:prefer-inline') @override $Res call({ - Object? attachment = null, + Object? attachment = freezed, }) { return _then(_$ConnectionStateImpl( - attachment: null == attachment + attachment: freezed == attachment ? _value.attachment : attachment // ignore: cast_nullable_to_non_nullable as VeilidStateAttachment, @@ -120,12 +107,13 @@ class _$ConnectionStateImpl extends _ConnectionState { return identical(this, other) || (other.runtimeType == runtimeType && other is _$ConnectionStateImpl && - (identical(other.attachment, attachment) || - other.attachment == attachment)); + const DeepCollectionEquality() + .equals(other.attachment, attachment)); } @override - int get hashCode => Object.hash(runtimeType, attachment); + int get hashCode => + Object.hash(runtimeType, const DeepCollectionEquality().hash(attachment)); @JsonKey(ignore: true) @override diff --git a/lib/old_to_refactor/providers/contact.dart b/lib/old_to_refactor/providers/contact.dart index 731d5a7..ced5c24 100644 --- a/lib/old_to_refactor/providers/contact.dart +++ b/lib/old_to_refactor/providers/contact.dart @@ -5,7 +5,7 @@ import 'package:riverpod_annotation/riverpod_annotation.dart'; import '../../proto/proto.dart' as proto; -import '../../veilid_support/veilid_support.dart'; +import '../../../packages/veilid_support/veilid_support.dart'; import '../../tools/tools.dart'; import 'account.dart'; import 'chat.dart'; diff --git a/lib/old_to_refactor/providers/contact_invitation_list_manager.dart b/lib/old_to_refactor/providers/contact_invitation_list_manager.dart index 099bc26..550169e 100644 --- a/lib/old_to_refactor/providers/contact_invitation_list_manager.dart +++ b/lib/old_to_refactor/providers/contact_invitation_list_manager.dart @@ -7,7 +7,7 @@ import 'package:mutex/mutex.dart'; import '../../entities/entities.dart'; import '../../proto/proto.dart' as proto; import '../../tools/tools.dart'; -import '../../veilid_support/veilid_support.dart'; +import '../../../packages/veilid_support/veilid_support.dart'; import 'account.dart'; part 'contact_invitation_list_manager.g.dart'; @@ -115,11 +115,11 @@ class ContactInvitationListManager extends _$ContactInvitationListManager { final conversationWriter = _activeAccountInfo.getConversationWriter(); // Encrypt the writer secret with the encryption key - final encryptedSecret = await encryptSecretToBytes( + final encryptedSecret = await encryptionKeyType.encryptSecretToBytes( secret: contactRequestWriter.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 diff --git a/lib/old_to_refactor/providers/contact_invite.dart b/lib/old_to_refactor/providers/contact_invite.dart index a8f5787..24e792a 100644 --- a/lib/old_to_refactor/providers/contact_invite.dart +++ b/lib/old_to_refactor/providers/contact_invite.dart @@ -7,7 +7,7 @@ import 'package:riverpod_annotation/riverpod_annotation.dart'; import '../../entities/local_account.dart'; import '../../proto/proto.dart' as proto; import '../../tools/tools.dart'; -import '../../veilid_support/veilid_support.dart'; +import '../../../packages/veilid_support/veilid_support.dart'; import 'account.dart'; import 'conversation.dart'; diff --git a/lib/old_to_refactor/providers/conversation.dart b/lib/old_to_refactor/providers/conversation.dart index aedd01b..451f8e3 100644 --- a/lib/old_to_refactor/providers/conversation.dart +++ b/lib/old_to_refactor/providers/conversation.dart @@ -11,7 +11,7 @@ import '../../proto/proto.dart' as proto; import '../../tools/tools.dart'; import '../../init.dart'; -import '../../veilid_support/veilid_support.dart'; +import '../../../packages/veilid_support/veilid_support.dart'; import 'account.dart'; import 'chat.dart'; import 'contact.dart'; diff --git a/lib/old_to_refactor/providers/window_control.dart b/lib/old_to_refactor/providers/window_control.dart deleted file mode 100644 index b6dfb76..0000000 --- a/lib/old_to_refactor/providers/window_control.dart +++ /dev/null @@ -1,83 +0,0 @@ -import 'dart:async'; - -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:riverpod_annotation/riverpod_annotation.dart'; -import 'package:window_manager/window_manager.dart'; - -import '../../tools/responsive.dart'; - -export 'package:window_manager/window_manager.dart' show TitleBarStyle; - -part 'window_control.g.dart'; - -enum OrientationCapability { - normal, - portraitOnly, - landscapeOnly, -} - -// Window Control -@riverpod -class WindowControl extends _$WindowControl { - /// Change window control - @override - FutureOr build() async { - await _doWindowSetup(TitleBarStyle.hidden, OrientationCapability.normal); - return true; - } - - static Future initialize() async { - if (isDesktop) { - await windowManager.ensureInitialized(); - - const windowOptions = WindowOptions( - size: Size(768, 1024), - //minimumSize: Size(480, 480), - center: true, - backgroundColor: Colors.transparent, - skipTaskbar: false, - ); - await windowManager.waitUntilReadyToShow(windowOptions, () async { - await windowManager.show(); - await windowManager.focus(); - }); - } - } - - Future _doWindowSetup(TitleBarStyle titleBarStyle, - OrientationCapability orientationCapability) async { - if (isDesktop) { - await windowManager.setTitleBarStyle(titleBarStyle); - } else { - switch (orientationCapability) { - case OrientationCapability.normal: - await SystemChrome.setPreferredOrientations([ - DeviceOrientation.portraitUp, - DeviceOrientation.landscapeLeft, - DeviceOrientation.landscapeRight, - ]); - case OrientationCapability.portraitOnly: - await SystemChrome.setPreferredOrientations([ - DeviceOrientation.portraitUp, - ]); - case OrientationCapability.landscapeOnly: - await SystemChrome.setPreferredOrientations([ - DeviceOrientation.landscapeLeft, - DeviceOrientation.landscapeRight, - ]); - } - } - } - - ////////////////////////////////////////////////////////////// - /// Mutators and Selectors - - /// Reorder accounts - Future changeWindowSetup(TitleBarStyle titleBarStyle, - OrientationCapability orientationCapability) async { - state = const AsyncValue.loading(); - await _doWindowSetup(titleBarStyle, orientationCapability); - state = const AsyncValue.data(true); - } -} diff --git a/lib/processor.dart b/lib/processor.dart index 36878fd..d0c359a 100644 --- a/lib/processor.dart +++ b/lib/processor.dart @@ -2,10 +2,11 @@ import 'dart:async'; import 'package:veilid/veilid.dart'; +import 'app.dart'; import 'old_to_refactor/providers/connection_state.dart'; import 'tools/tools.dart'; -import 'veilid_support/src/config.dart'; -import 'veilid_support/src/veilid_log.dart'; +import '../packages/veilid_support/src/config.dart'; +import '../packages/veilid_support/src/veilid_log.dart'; class Processor { Processor(); @@ -27,13 +28,15 @@ class Processor { log.info('Veilid version: $_veilidVersion'); - // In case of hot restart shut down first + // HACK: In case of hot restart shut down first try { await Veilid.instance.shutdownVeilidCore(); - } on Exception {} + } on Exception { + // Do nothing on failure here + } - final updateStream = - await Veilid.instance.startupVeilidCore(await getVeilidConfig()); + final updateStream = await Veilid.instance + .startupVeilidCore(await getVeilidConfig(VeilidChatApp.name)); _updateStream = updateStream; _updateProcessor = processUpdates(); _startedUp = true; diff --git a/lib/proto/proto.dart b/lib/proto/proto.dart index 9d1aeb6..cfccda3 100644 --- a/lib/proto/proto.dart +++ b/lib/proto/proto.dart @@ -1,5 +1,5 @@ -export '../veilid_support/dht_support/proto/proto.dart'; -export '../veilid_support/proto/proto.dart'; +export 'package:veilid_support/dht_support/proto/proto.dart'; +export 'package:veilid_support/proto/proto.dart'; export 'veilidchat.pb.dart'; export 'veilidchat.pbenum.dart'; export 'veilidchat.pbjson.dart'; diff --git a/lib/router/cubit/router_cubit.dart b/lib/router/cubit/router_cubit.dart index 70936e7..5fcd379 100644 --- a/lib/router/cubit/router_cubit.dart +++ b/lib/router/cubit/router_cubit.dart @@ -6,12 +6,12 @@ import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:go_router/go_router.dart'; import '../../init.dart'; -import '../../local_account_manager/account_repository/account_repository.dart'; +import '../../local_account_manager/respository/account_repository/account_repository.dart'; import '../../old_to_refactor/pages/chat_only.dart'; import '../../old_to_refactor/pages/developer.dart'; import '../../old_to_refactor/pages/home.dart'; import '../../old_to_refactor/pages/index.dart'; -import '../../old_to_refactor/pages/new_account.dart'; +import '../../account_manager/view/new_account_page/new_account_page.dart'; import '../../old_to_refactor/pages/settings.dart'; import '../../tools/tools.dart'; diff --git a/lib/router/cubit/router_cubit.freezed.dart b/lib/router/cubit/router_cubit.freezed.dart deleted file mode 100644 index a79e746..0000000 --- a/lib/router/cubit/router_cubit.freezed.dart +++ /dev/null @@ -1,193 +0,0 @@ -// coverage:ignore-file -// GENERATED CODE - DO NOT MODIFY BY HAND -// ignore_for_file: type=lint -// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark - -part of 'router_cubit.dart'; - -// ************************************************************************** -// FreezedGenerator -// ************************************************************************** - -T _$identity(T value) => value; - -final _privateConstructorUsedError = UnsupportedError( - 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#custom-getters-and-methods'); - -RouterState _$RouterStateFromJson(Map json) { - return _RouterState.fromJson(json); -} - -/// @nodoc -mixin _$RouterState { - bool get isInitialized => throw _privateConstructorUsedError; - bool get hasAnyAccount => throw _privateConstructorUsedError; - bool get hasActiveChat => throw _privateConstructorUsedError; - - Map toJson() => throw _privateConstructorUsedError; - @JsonKey(ignore: true) - $RouterStateCopyWith get copyWith => - throw _privateConstructorUsedError; -} - -/// @nodoc -abstract class $RouterStateCopyWith<$Res> { - factory $RouterStateCopyWith( - RouterState value, $Res Function(RouterState) then) = - _$RouterStateCopyWithImpl<$Res, RouterState>; - @useResult - $Res call({bool isInitialized, bool hasAnyAccount, bool hasActiveChat}); -} - -/// @nodoc -class _$RouterStateCopyWithImpl<$Res, $Val extends RouterState> - implements $RouterStateCopyWith<$Res> { - _$RouterStateCopyWithImpl(this._value, this._then); - - // ignore: unused_field - final $Val _value; - // ignore: unused_field - final $Res Function($Val) _then; - - @pragma('vm:prefer-inline') - @override - $Res call({ - Object? isInitialized = null, - Object? hasAnyAccount = null, - Object? hasActiveChat = null, - }) { - return _then(_value.copyWith( - isInitialized: null == isInitialized - ? _value.isInitialized - : isInitialized // ignore: cast_nullable_to_non_nullable - as bool, - hasAnyAccount: null == hasAnyAccount - ? _value.hasAnyAccount - : hasAnyAccount // ignore: cast_nullable_to_non_nullable - as bool, - hasActiveChat: null == hasActiveChat - ? _value.hasActiveChat - : hasActiveChat // ignore: cast_nullable_to_non_nullable - as bool, - ) as $Val); - } -} - -/// @nodoc -abstract class _$$RouterStateImplCopyWith<$Res> - implements $RouterStateCopyWith<$Res> { - factory _$$RouterStateImplCopyWith( - _$RouterStateImpl value, $Res Function(_$RouterStateImpl) then) = - __$$RouterStateImplCopyWithImpl<$Res>; - @override - @useResult - $Res call({bool isInitialized, bool hasAnyAccount, bool hasActiveChat}); -} - -/// @nodoc -class __$$RouterStateImplCopyWithImpl<$Res> - extends _$RouterStateCopyWithImpl<$Res, _$RouterStateImpl> - implements _$$RouterStateImplCopyWith<$Res> { - __$$RouterStateImplCopyWithImpl( - _$RouterStateImpl _value, $Res Function(_$RouterStateImpl) _then) - : super(_value, _then); - - @pragma('vm:prefer-inline') - @override - $Res call({ - Object? isInitialized = null, - Object? hasAnyAccount = null, - Object? hasActiveChat = null, - }) { - return _then(_$RouterStateImpl( - isInitialized: null == isInitialized - ? _value.isInitialized - : isInitialized // ignore: cast_nullable_to_non_nullable - as bool, - hasAnyAccount: null == hasAnyAccount - ? _value.hasAnyAccount - : hasAnyAccount // ignore: cast_nullable_to_non_nullable - as bool, - hasActiveChat: null == hasActiveChat - ? _value.hasActiveChat - : hasActiveChat // ignore: cast_nullable_to_non_nullable - as bool, - )); - } -} - -/// @nodoc -@JsonSerializable() -class _$RouterStateImpl implements _RouterState { - const _$RouterStateImpl( - {required this.isInitialized, - required this.hasAnyAccount, - required this.hasActiveChat}); - - factory _$RouterStateImpl.fromJson(Map json) => - _$$RouterStateImplFromJson(json); - - @override - final bool isInitialized; - @override - final bool hasAnyAccount; - @override - final bool hasActiveChat; - - @override - String toString() { - return 'RouterState(isInitialized: $isInitialized, hasAnyAccount: $hasAnyAccount, hasActiveChat: $hasActiveChat)'; - } - - @override - bool operator ==(Object other) { - return identical(this, other) || - (other.runtimeType == runtimeType && - other is _$RouterStateImpl && - (identical(other.isInitialized, isInitialized) || - other.isInitialized == isInitialized) && - (identical(other.hasAnyAccount, hasAnyAccount) || - other.hasAnyAccount == hasAnyAccount) && - (identical(other.hasActiveChat, hasActiveChat) || - other.hasActiveChat == hasActiveChat)); - } - - @JsonKey(ignore: true) - @override - int get hashCode => - Object.hash(runtimeType, isInitialized, hasAnyAccount, hasActiveChat); - - @JsonKey(ignore: true) - @override - @pragma('vm:prefer-inline') - _$$RouterStateImplCopyWith<_$RouterStateImpl> get copyWith => - __$$RouterStateImplCopyWithImpl<_$RouterStateImpl>(this, _$identity); - - @override - Map toJson() { - return _$$RouterStateImplToJson( - this, - ); - } -} - -abstract class _RouterState implements RouterState { - const factory _RouterState( - {required final bool isInitialized, - required final bool hasAnyAccount, - required final bool hasActiveChat}) = _$RouterStateImpl; - - factory _RouterState.fromJson(Map json) = - _$RouterStateImpl.fromJson; - - @override - bool get isInitialized; - @override - bool get hasAnyAccount; - @override - bool get hasActiveChat; - @override - @JsonKey(ignore: true) - _$$RouterStateImplCopyWith<_$RouterStateImpl> get copyWith => - throw _privateConstructorUsedError; -} diff --git a/lib/router/cubit/router_cubit.g.dart b/lib/router/cubit/router_cubit.g.dart deleted file mode 100644 index f67c770..0000000 --- a/lib/router/cubit/router_cubit.g.dart +++ /dev/null @@ -1,21 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'router_cubit.dart'; - -// ************************************************************************** -// JsonSerializableGenerator -// ************************************************************************** - -_$RouterStateImpl _$$RouterStateImplFromJson(Map json) => - _$RouterStateImpl( - isInitialized: json['is_initialized'] as bool, - hasAnyAccount: json['has_any_account'] as bool, - hasActiveChat: json['has_active_chat'] as bool, - ); - -Map _$$RouterStateImplToJson(_$RouterStateImpl instance) => - { - 'is_initialized': instance.isInitialized, - 'has_any_account': instance.hasAnyAccount, - 'has_active_chat': instance.hasActiveChat, - }; diff --git a/lib/theme/theme_service.dart b/lib/theme/theme_service.dart deleted file mode 100644 index 5464c04..0000000 --- a/lib/theme/theme_service.dart +++ /dev/null @@ -1,108 +0,0 @@ -// ignore_for_file: always_put_required_named_parameters_first - -import 'dart:convert'; - -import 'package:flutter/material.dart'; -import 'package:shared_preferences/shared_preferences.dart'; - -import '../entities/preferences.dart'; -import 'radix_generator.dart'; - -//////////////////////////////////////////////////////////////////////// - -class ThemeService { - ThemeService._(); - static late SharedPreferences prefs; - static ThemeService? _instance; - - static Future get instance async { - if (_instance == null) { - prefs = await SharedPreferences.getInstance(); - _instance = ThemeService._(); - } - return _instance!; - } - - static bool get isPlatformDark => - WidgetsBinding.instance.platformDispatcher.platformBrightness == - Brightness.dark; - - ThemeData get initial { - final themePreferences = load(); - return get(themePreferences); - } - - ThemePreferences load() { - final themePreferencesJson = prefs.getString('themePreferences'); - ThemePreferences? themePreferences; - if (themePreferencesJson != null) { - try { - themePreferences = - ThemePreferences.fromJson(jsonDecode(themePreferencesJson)); - // ignore: avoid_catches_without_on_clauses - } catch (_) { - // ignore - } - } - return themePreferences ?? - const ThemePreferences( - colorPreference: ColorPreference.vapor, - brightnessPreference: BrightnessPreference.system, - displayScale: 1, - ); - } - - Future save(ThemePreferences themePreferences) async { - await prefs.setString( - 'themePreferences', jsonEncode(themePreferences.toJson())); - } - - ThemeData get(ThemePreferences themePreferences) { - late final Brightness brightness; - switch (themePreferences.brightnessPreference) { - case BrightnessPreference.system: - if (isPlatformDark) { - brightness = Brightness.dark; - } else { - brightness = Brightness.light; - } - case BrightnessPreference.light: - brightness = Brightness.light; - case BrightnessPreference.dark: - brightness = Brightness.dark; - } - - late final ThemeData themeData; - switch (themePreferences.colorPreference) { - // Special cases - case ColorPreference.contrast: - // xxx do contrastGenerator - themeData = radixGenerator(brightness, RadixThemeColor.grim); - // Generate from Radix - case ColorPreference.scarlet: - themeData = radixGenerator(brightness, RadixThemeColor.scarlet); - case ColorPreference.babydoll: - themeData = radixGenerator(brightness, RadixThemeColor.babydoll); - case ColorPreference.vapor: - themeData = radixGenerator(brightness, RadixThemeColor.vapor); - case ColorPreference.gold: - themeData = radixGenerator(brightness, RadixThemeColor.gold); - case ColorPreference.garden: - themeData = radixGenerator(brightness, RadixThemeColor.garden); - case ColorPreference.forest: - themeData = radixGenerator(brightness, RadixThemeColor.forest); - case ColorPreference.arctic: - themeData = radixGenerator(brightness, RadixThemeColor.arctic); - case ColorPreference.lapis: - themeData = radixGenerator(brightness, RadixThemeColor.lapis); - case ColorPreference.eggplant: - themeData = radixGenerator(brightness, RadixThemeColor.eggplant); - case ColorPreference.lime: - themeData = radixGenerator(brightness, RadixThemeColor.lime); - case ColorPreference.grim: - themeData = radixGenerator(brightness, RadixThemeColor.grim); - } - - return themeData; - } -} diff --git a/lib/tick.dart b/lib/tick.dart index bcaa478..82909bc 100644 --- a/lib/tick.dart +++ b/lib/tick.dart @@ -5,14 +5,14 @@ import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import 'proto/proto.dart' as proto; +import 'init.dart'; import 'old_to_refactor/providers/account.dart'; import 'old_to_refactor/providers/chat.dart'; import 'old_to_refactor/providers/connection_state.dart'; import 'old_to_refactor/providers/contact.dart'; import 'old_to_refactor/providers/contact_invite.dart'; import 'old_to_refactor/providers/conversation.dart'; -import 'init.dart'; +import 'proto/proto.dart' as proto; const int ticksPerContactInvitationCheck = 5; const int ticksPerNewMessageCheck = 5; diff --git a/lib/tools/async_table_db_backed_cubit.dart b/lib/tools/async_table_db_backed_cubit.dart index b3aac2c..d8183a7 100644 --- a/lib/tools/async_table_db_backed_cubit.dart +++ b/lib/tools/async_table_db_backed_cubit.dart @@ -4,7 +4,7 @@ import 'package:bloc/bloc.dart'; import '../../tools/tools.dart'; import '../init.dart'; -import '../../veilid_support/veilid_support.dart'; +import '../../packages/veilid_support/veilid_support.dart'; abstract class AsyncTableDBBackedCubit extends Cubit> with TableDBBacked { diff --git a/lib/tools/loggy.dart b/lib/tools/loggy.dart index 0a7f6a5..3be0f5c 100644 --- a/lib/tools/loggy.dart +++ b/lib/tools/loggy.dart @@ -8,7 +8,7 @@ import 'package:intl/intl.dart'; import 'package:loggy/loggy.dart'; import '../old_to_refactor/pages/developer.dart'; -import '../veilid_support/veilid_support.dart'; +import '../../packages/veilid_support/veilid_support.dart'; import 'state_logger.dart'; String wrapWithLogColor(LogLevel? level, String text) { diff --git a/lib/tools/secret_crypto.dart b/lib/tools/secret_crypto.dart deleted file mode 100644 index 0920be7..0000000 --- a/lib/tools/secret_crypto.dart +++ /dev/null @@ -1,46 +0,0 @@ -import 'dart:typed_data'; -import '../local_accounts/local_account.dart'; -import '../init.dart'; -import '../veilid_support/veilid_support.dart'; - -Future encryptSecretToBytes( - {required SecretKey secret, - required CryptoKind cryptoKind, - EncryptionKeyType encryptionKeyType = EncryptionKeyType.none, - String encryptionKey = ''}) async { - final veilid = await eventualVeilid.future; - - late final Uint8List secretBytes; - switch (encryptionKeyType) { - case EncryptionKeyType.none: - secretBytes = secret.decode(); - case EncryptionKeyType.pin: - case EncryptionKeyType.password: - final cs = await veilid.getCryptoSystem(cryptoKind); - - secretBytes = - await cs.encryptAeadWithPassword(secret.decode(), encryptionKey); - } - return secretBytes; -} - -Future decryptSecretFromBytes( - {required Uint8List secretBytes, - required CryptoKind cryptoKind, - EncryptionKeyType encryptionKeyType = EncryptionKeyType.none, - String encryptionKey = ''}) async { - final veilid = await eventualVeilid.future; - - late final SecretKey secret; - switch (encryptionKeyType) { - case EncryptionKeyType.none: - secret = SecretKey.fromBytes(secretBytes); - case EncryptionKeyType.pin: - case EncryptionKeyType.password: - final cs = await veilid.getCryptoSystem(cryptoKind); - - secret = SecretKey.fromBytes( - await cs.decryptAeadWithPassword(secretBytes, encryptionKey)); - } - return secret; -} diff --git a/lib/tools/tools.dart b/lib/tools/tools.dart index 34af70d..203cfc3 100644 --- a/lib/tools/tools.dart +++ b/lib/tools/tools.dart @@ -5,6 +5,6 @@ export 'loggy.dart'; export 'phono_byte.dart'; export 'responsive.dart'; export 'scanner_error_widget.dart'; -export 'secret_crypto.dart'; export 'state_logger.dart'; export 'widget_helpers.dart'; +export 'window_control.dart'; diff --git a/lib/tools/window_control.dart b/lib/tools/window_control.dart new file mode 100644 index 0000000..d3be316 --- /dev/null +++ b/lib/tools/window_control.dart @@ -0,0 +1,65 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:window_manager/window_manager.dart'; + +import '../../tools/responsive.dart'; + +export 'package:window_manager/window_manager.dart' show TitleBarStyle; + +enum OrientationCapability { + normal, + portraitOnly, + landscapeOnly, +} + +// Window Control +Future initializeWindowControl() async { + if (isDesktop) { + await windowManager.ensureInitialized(); + + const windowOptions = WindowOptions( + size: Size(768, 1024), + //minimumSize: Size(480, 480), + center: true, + backgroundColor: Colors.transparent, + skipTaskbar: false, + ); + await windowManager.waitUntilReadyToShow(windowOptions, () async { + await _doWindowSetup(TitleBarStyle.hidden, OrientationCapability.normal); + await windowManager.show(); + await windowManager.focus(); + }); + } +} + +Future _doWindowSetup(TitleBarStyle titleBarStyle, + OrientationCapability orientationCapability) async { + if (isDesktop) { + await windowManager.setTitleBarStyle(titleBarStyle); + } else { + switch (orientationCapability) { + case OrientationCapability.normal: + await SystemChrome.setPreferredOrientations([ + DeviceOrientation.portraitUp, + DeviceOrientation.landscapeLeft, + DeviceOrientation.landscapeRight, + ]); + case OrientationCapability.portraitOnly: + await SystemChrome.setPreferredOrientations([ + DeviceOrientation.portraitUp, + ]); + case OrientationCapability.landscapeOnly: + await SystemChrome.setPreferredOrientations([ + DeviceOrientation.landscapeLeft, + DeviceOrientation.landscapeRight, + ]); + } + } +} + +Future changeWindowSetup(TitleBarStyle titleBarStyle, + OrientationCapability orientationCapability) async { + await _doWindowSetup(titleBarStyle, orientationCapability); +} diff --git a/packages/veilid_support/.gitignore b/packages/veilid_support/.gitignore new file mode 100644 index 0000000..df26946 --- /dev/null +++ b/packages/veilid_support/.gitignore @@ -0,0 +1,56 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +**/ios/Flutter/.last_build_id +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies +.packages +.pub-cache/ +.pub/ +/build/ + +# Flutter generated files +# Not doing this at this time: https://stackoverflow.com/questions/56110386/should-i-commit-generated-code-in-flutter-dart-to-vcs +# *.g.dart +# *.freezed.dart +# *.pb.dart +# *.pbenum.dart +# *.pbjson.dart +# *.pbserver.dart + +# Symbolication related +app.*.symbols + +# Obfuscation related +app.*.map.json + +# Android Studio will place build artifacts here +/android/app/debug +/android/app/profile +/android/app/release + +# WASM +/web/wasm/ diff --git a/packages/veilid_support/analysis_options.yaml b/packages/veilid_support/analysis_options.yaml new file mode 100644 index 0000000..e1620f7 --- /dev/null +++ b/packages/veilid_support/analysis_options.yaml @@ -0,0 +1,15 @@ +include: package:lint_hard/all.yaml +analyzer: + errors: + invalid_annotation_target: ignore + exclude: + - '**/*.g.dart' + - '**/*.freezed.dart' + - '**/*.pb.dart' + - '**/*.pbenum.dart' + - '**/*.pbjson.dart' + - '**/*.pbserver.dart' +linter: + rules: + unawaited_futures: true + avoid_positional_boolean_parameters: false \ No newline at end of file diff --git a/packages/veilid_support/build.bat b/packages/veilid_support/build.bat new file mode 100644 index 0000000..88d2bb0 --- /dev/null +++ b/packages/veilid_support/build.bat @@ -0,0 +1,7 @@ +@echo off +dart run build_runner build --delete-conflicting-outputs + +pushd lib +protoc --dart_out=proto -I proto -I dht_support\proto dht.proto +protoc --dart_out=proto -I proto veilid.proto +popd diff --git a/packages/veilid_support/build.sh b/packages/veilid_support/build.sh new file mode 100755 index 0000000..0c43bc6 --- /dev/null +++ b/packages/veilid_support/build.sh @@ -0,0 +1,8 @@ +#!/bin/bash +set -e +dart run build_runner build --delete-conflicting-outputs + +pushd lib > /dev/null +protoc --dart_out=proto -I proto -I dht_support/proto dht.proto +protoc --dart_out=proto -I proto veilid.proto +popd > /dev/null diff --git a/lib/veilid_support/dht_support/dht_support.dart b/packages/veilid_support/lib/dht_support/dht_support.dart similarity index 100% rename from lib/veilid_support/dht_support/dht_support.dart rename to packages/veilid_support/lib/dht_support/dht_support.dart diff --git a/lib/veilid_support/dht_support/proto/dht.proto b/packages/veilid_support/lib/dht_support/proto/dht.proto similarity index 100% rename from lib/veilid_support/dht_support/proto/dht.proto rename to packages/veilid_support/lib/dht_support/proto/dht.proto diff --git a/lib/veilid_support/dht_support/proto/proto.dart b/packages/veilid_support/lib/dht_support/proto/proto.dart similarity index 75% rename from lib/veilid_support/dht_support/proto/proto.dart rename to packages/veilid_support/lib/dht_support/proto/proto.dart index f4244c7..f61e342 100644 --- a/lib/veilid_support/dht_support/proto/proto.dart +++ b/packages/veilid_support/lib/dht_support/proto/proto.dart @@ -1,11 +1,11 @@ -import '../../../proto/dht.pb.dart' as dhtproto; +import '../../proto/dht.pb.dart' as dhtproto; import '../../proto/proto.dart' as veilidproto; import '../dht_support.dart'; -export '../../../proto/dht.pb.dart'; -export '../../../proto/dht.pbenum.dart'; -export '../../../proto/dht.pbjson.dart'; -export '../../../proto/dht.pbserver.dart'; +export '../../proto/dht.pb.dart'; +export '../../proto/dht.pbenum.dart'; +export '../../proto/dht.pbjson.dart'; +export '../../proto/dht.pbserver.dart'; export '../../proto/proto.dart'; /// OwnedDHTRecordPointer protobuf marshaling diff --git a/lib/veilid_support/dht_support/src/dht_record.dart b/packages/veilid_support/lib/dht_support/src/dht_record.dart similarity index 99% rename from lib/veilid_support/dht_support/src/dht_record.dart rename to packages/veilid_support/lib/dht_support/src/dht_record.dart index 3722027..bc6dea2 100644 --- a/lib/veilid_support/dht_support/src/dht_record.dart +++ b/packages/veilid_support/lib/dht_support/src/dht_record.dart @@ -3,7 +3,7 @@ import 'dart:typed_data'; import 'package:protobuf/protobuf.dart'; -import '../../veilid_support.dart'; +import '../../../../veilid_support.dart'; class DHTRecord { DHTRecord( diff --git a/lib/veilid_support/dht_support/src/dht_record_crypto.dart b/packages/veilid_support/lib/dht_support/src/dht_record_crypto.dart similarity index 97% rename from lib/veilid_support/dht_support/src/dht_record_crypto.dart rename to packages/veilid_support/lib/dht_support/src/dht_record_crypto.dart index 41a8949..7d59453 100644 --- a/lib/veilid_support/dht_support/src/dht_record_crypto.dart +++ b/packages/veilid_support/lib/dht_support/src/dht_record_crypto.dart @@ -1,6 +1,6 @@ import 'dart:async'; import 'dart:typed_data'; -import '../../veilid_support.dart'; +import '../../../../veilid_support.dart'; abstract class DHTRecordCrypto { FutureOr encrypt(Uint8List data, int subkey); diff --git a/lib/veilid_support/dht_support/src/dht_record_pool.dart b/packages/veilid_support/lib/dht_support/src/dht_record_pool.dart similarity index 99% rename from lib/veilid_support/dht_support/src/dht_record_pool.dart rename to packages/veilid_support/lib/dht_support/src/dht_record_pool.dart index 88edf37..33960bb 100644 --- a/lib/veilid_support/dht_support/src/dht_record_pool.dart +++ b/packages/veilid_support/lib/dht_support/src/dht_record_pool.dart @@ -2,7 +2,7 @@ import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:mutex/mutex.dart'; -import '../../veilid_support.dart'; +import '../../../../veilid_support.dart'; part 'dht_record_pool.freezed.dart'; part 'dht_record_pool.g.dart'; diff --git a/lib/veilid_support/dht_support/src/dht_record_pool.freezed.dart b/packages/veilid_support/lib/dht_support/src/dht_record_pool.freezed.dart similarity index 100% rename from lib/veilid_support/dht_support/src/dht_record_pool.freezed.dart rename to packages/veilid_support/lib/dht_support/src/dht_record_pool.freezed.dart diff --git a/lib/veilid_support/dht_support/src/dht_record_pool.g.dart b/packages/veilid_support/lib/dht_support/src/dht_record_pool.g.dart similarity index 75% rename from lib/veilid_support/dht_support/src/dht_record_pool.g.dart rename to packages/veilid_support/lib/dht_support/src/dht_record_pool.g.dart index b7bb9c2..ea2a61b 100644 --- a/lib/veilid_support/dht_support/src/dht_record_pool.g.dart +++ b/packages/veilid_support/lib/dht_support/src/dht_record_pool.g.dart @@ -11,47 +11,47 @@ _$DHTRecordPoolAllocationsImpl _$$DHTRecordPoolAllocationsImplFromJson( _$DHTRecordPoolAllocationsImpl( childrenByParent: IMap>>.fromJson( - json['children_by_parent'] as Map, + json['childrenByParent'] as Map, (value) => value as String, (value) => ISet>.fromJson(value, (value) => Typed.fromJson(value))), parentByChild: IMap>.fromJson( - json['parent_by_child'] as Map, + json['parentByChild'] as Map, (value) => value as String, (value) => Typed.fromJson(value)), rootRecords: ISet>.fromJson( - json['root_records'], + json['rootRecords'], (value) => Typed.fromJson(value)), ); Map _$$DHTRecordPoolAllocationsImplToJson( _$DHTRecordPoolAllocationsImpl instance) => { - 'children_by_parent': instance.childrenByParent.toJson( + 'childrenByParent': instance.childrenByParent.toJson( (value) => value, (value) => value.toJson( - (value) => value.toJson(), + (value) => value, ), ), - 'parent_by_child': instance.parentByChild.toJson( + 'parentByChild': instance.parentByChild.toJson( + (value) => value, (value) => value, - (value) => value.toJson(), ), - 'root_records': instance.rootRecords.toJson( - (value) => value.toJson(), + 'rootRecords': instance.rootRecords.toJson( + (value) => value, ), }; _$OwnedDHTRecordPointerImpl _$$OwnedDHTRecordPointerImplFromJson( Map json) => _$OwnedDHTRecordPointerImpl( - recordKey: Typed.fromJson(json['record_key']), + recordKey: Typed.fromJson(json['recordKey']), owner: KeyPair.fromJson(json['owner']), ); Map _$$OwnedDHTRecordPointerImplToJson( _$OwnedDHTRecordPointerImpl instance) => { - 'record_key': instance.recordKey.toJson(), - 'owner': instance.owner.toJson(), + 'recordKey': instance.recordKey, + 'owner': instance.owner, }; diff --git a/lib/veilid_support/dht_support/src/dht_short_array.dart b/packages/veilid_support/lib/dht_support/src/dht_short_array.dart similarity index 99% rename from lib/veilid_support/dht_support/src/dht_short_array.dart rename to packages/veilid_support/lib/dht_support/src/dht_short_array.dart index 82b701f..f115e5a 100644 --- a/lib/veilid_support/dht_support/src/dht_short_array.dart +++ b/packages/veilid_support/lib/dht_support/src/dht_short_array.dart @@ -3,7 +3,7 @@ import 'dart:typed_data'; import 'package:protobuf/protobuf.dart'; -import '../../veilid_support.dart'; +import '../../../../veilid_support.dart'; import '../proto/proto.dart' as proto; class _DHTShortArrayCache { diff --git a/lib/proto/dht.pb.dart b/packages/veilid_support/lib/proto/dht.pb.dart similarity index 100% rename from lib/proto/dht.pb.dart rename to packages/veilid_support/lib/proto/dht.pb.dart diff --git a/lib/proto/dht.pbenum.dart b/packages/veilid_support/lib/proto/dht.pbenum.dart similarity index 100% rename from lib/proto/dht.pbenum.dart rename to packages/veilid_support/lib/proto/dht.pbenum.dart diff --git a/lib/proto/dht.pbjson.dart b/packages/veilid_support/lib/proto/dht.pbjson.dart similarity index 100% rename from lib/proto/dht.pbjson.dart rename to packages/veilid_support/lib/proto/dht.pbjson.dart diff --git a/lib/proto/dht.pbserver.dart b/packages/veilid_support/lib/proto/dht.pbserver.dart similarity index 100% rename from lib/proto/dht.pbserver.dart rename to packages/veilid_support/lib/proto/dht.pbserver.dart diff --git a/lib/veilid_support/proto/proto.dart b/packages/veilid_support/lib/proto/proto.dart similarity index 75% rename from lib/veilid_support/proto/proto.dart rename to packages/veilid_support/lib/proto/proto.dart index 941c2af..86f3606 100644 --- a/lib/veilid_support/proto/proto.dart +++ b/packages/veilid_support/lib/proto/proto.dart @@ -1,16 +1,16 @@ import 'dart:typed_data'; -import '../../proto/veilid.pb.dart' as proto; -import '../veilid_support.dart'; +import '../veilid_support.dart' as veilid; +import 'veilid.pb.dart' as proto; -export '../../proto/veilid.pb.dart'; -export '../../proto/veilid.pbenum.dart'; -export '../../proto/veilid.pbjson.dart'; -export '../../proto/veilid.pbserver.dart'; +export 'veilid.pb.dart'; +export 'veilid.pbenum.dart'; +export 'veilid.pbjson.dart'; +export 'veilid.pbserver.dart'; /// CryptoKey protobuf marshaling /// -extension CryptoKeyProto on CryptoKey { +extension CryptoKeyProto on veilid.CryptoKey { proto.CryptoKey toProto() { final b = decode().buffer.asByteData(); final out = proto.CryptoKey() @@ -25,7 +25,7 @@ extension CryptoKeyProto on CryptoKey { return out; } - static CryptoKey fromProto(proto.CryptoKey p) { + static veilid.CryptoKey fromProto(proto.CryptoKey p) { final b = ByteData(32) ..setUint32(0 * 4, p.u0) ..setUint32(1 * 4, p.u1) @@ -35,13 +35,13 @@ extension CryptoKeyProto on CryptoKey { ..setUint32(5 * 4, p.u5) ..setUint32(6 * 4, p.u6) ..setUint32(7 * 4, p.u7); - return CryptoKey.fromBytes(Uint8List.view(b.buffer)); + return veilid.CryptoKey.fromBytes(Uint8List.view(b.buffer)); } } /// Signature protobuf marshaling /// -extension SignatureProto on Signature { +extension SignatureProto on veilid.Signature { proto.Signature toProto() { final b = decode().buffer.asByteData(); final out = proto.Signature() @@ -64,7 +64,7 @@ extension SignatureProto on Signature { return out; } - static Signature fromProto(proto.Signature p) { + static veilid.Signature fromProto(proto.Signature p) { final b = ByteData(64) ..setUint32(0 * 4, p.u0) ..setUint32(1 * 4, p.u1) @@ -82,13 +82,13 @@ extension SignatureProto on Signature { ..setUint32(13 * 4, p.u13) ..setUint32(14 * 4, p.u14) ..setUint32(15 * 4, p.u15); - return Signature.fromBytes(Uint8List.view(b.buffer)); + return veilid.Signature.fromBytes(Uint8List.view(b.buffer)); } } /// Nonce protobuf marshaling /// -extension NonceProto on Nonce { +extension NonceProto on veilid.Nonce { proto.Nonce toProto() { final b = decode().buffer.asByteData(); final out = proto.Nonce() @@ -101,7 +101,7 @@ extension NonceProto on Nonce { return out; } - static Nonce fromProto(proto.Nonce p) { + static veilid.Nonce fromProto(proto.Nonce p) { final b = ByteData(24) ..setUint32(0 * 4, p.u0) ..setUint32(1 * 4, p.u1) @@ -109,13 +109,13 @@ extension NonceProto on Nonce { ..setUint32(3 * 4, p.u3) ..setUint32(4 * 4, p.u4) ..setUint32(5 * 4, p.u5); - return Nonce.fromBytes(Uint8List.view(b.buffer)); + return veilid.Nonce.fromBytes(Uint8List.view(b.buffer)); } } /// TypedKey protobuf marshaling /// -extension TypedKeyProto on TypedKey { +extension TypedKeyProto on veilid.TypedKey { proto.TypedKey toProto() { final out = proto.TypedKey() ..kind = kind @@ -123,13 +123,13 @@ extension TypedKeyProto on TypedKey { return out; } - static TypedKey fromProto(proto.TypedKey p) => - TypedKey(kind: p.kind, value: CryptoKeyProto.fromProto(p.value)); + static veilid.TypedKey fromProto(proto.TypedKey p) => + veilid.TypedKey(kind: p.kind, value: CryptoKeyProto.fromProto(p.value)); } /// KeyPair protobuf marshaling /// -extension KeyPairProto on KeyPair { +extension KeyPairProto on veilid.KeyPair { proto.KeyPair toProto() { final out = proto.KeyPair() ..key = key.toProto() @@ -137,7 +137,7 @@ extension KeyPairProto on KeyPair { return out; } - static KeyPair fromProto(proto.KeyPair p) => KeyPair( + static veilid.KeyPair fromProto(proto.KeyPair p) => veilid.KeyPair( key: CryptoKeyProto.fromProto(p.key), secret: CryptoKeyProto.fromProto(p.secret)); } diff --git a/lib/proto/veilid.pb.dart b/packages/veilid_support/lib/proto/veilid.pb.dart similarity index 100% rename from lib/proto/veilid.pb.dart rename to packages/veilid_support/lib/proto/veilid.pb.dart diff --git a/lib/proto/veilid.pbenum.dart b/packages/veilid_support/lib/proto/veilid.pbenum.dart similarity index 100% rename from lib/proto/veilid.pbenum.dart rename to packages/veilid_support/lib/proto/veilid.pbenum.dart diff --git a/lib/proto/veilid.pbjson.dart b/packages/veilid_support/lib/proto/veilid.pbjson.dart similarity index 100% rename from lib/proto/veilid.pbjson.dart rename to packages/veilid_support/lib/proto/veilid.pbjson.dart diff --git a/lib/proto/veilid.pbserver.dart b/packages/veilid_support/lib/proto/veilid.pbserver.dart similarity index 100% rename from lib/proto/veilid.pbserver.dart rename to packages/veilid_support/lib/proto/veilid.pbserver.dart diff --git a/lib/veilid_support/proto/veilid.proto b/packages/veilid_support/lib/proto/veilid.proto similarity index 100% rename from lib/veilid_support/proto/veilid.proto rename to packages/veilid_support/lib/proto/veilid.proto diff --git a/lib/veilid_support/src/config.dart b/packages/veilid_support/lib/src/config.dart similarity index 93% rename from lib/veilid_support/src/config.dart rename to packages/veilid_support/lib/src/config.dart index 3e3aa76..99860a0 100644 --- a/lib/veilid_support/src/config.dart +++ b/packages/veilid_support/lib/src/config.dart @@ -1,8 +1,8 @@ -import 'package:flutter/foundation.dart'; import 'package:veilid/veilid.dart'; -Map getDefaultVeilidPlatformConfig(String appName) { - if (kIsWeb) { +Map getDefaultVeilidPlatformConfig( + bool isWeb, String appName) { + if (isWeb) { return const VeilidWASMConfig( logging: VeilidWASMConfigLogging( performance: VeilidWASMConfigLoggingPerformance( @@ -30,7 +30,7 @@ Map getDefaultVeilidPlatformConfig(String appName) { .toJson(); } -Future getVeilidConfig(String appName) async { +Future getVeilidConfig(bool isWeb, String appName) async { var config = await getDefaultVeilidConfig(appName); // ignore: do_not_use_environment if (const String.fromEnvironment('DELETE_TABLE_STORE') == '1') { @@ -51,7 +51,7 @@ Future getVeilidConfig(String appName) async { // ignore: do_not_use_environment const envNetwork = String.fromEnvironment('NETWORK'); if (envNetwork.isNotEmpty) { - final bootstrap = kIsWeb + final bootstrap = isWeb ? ['ws://bootstrap.$envNetwork.veilid.net:5150/ws'] : ['bootstrap.$envNetwork.veilid.net']; config = config.copyWith( diff --git a/lib/veilid_support/src/identity.dart b/packages/veilid_support/lib/src/identity.dart similarity index 99% rename from lib/veilid_support/src/identity.dart rename to packages/veilid_support/lib/src/identity.dart index 0baf34b..c4162df 100644 --- a/lib/veilid_support/src/identity.dart +++ b/packages/veilid_support/lib/src/identity.dart @@ -3,8 +3,9 @@ import 'dart:typed_data'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:protobuf/protobuf.dart'; +import 'package:veilid/veilid.dart'; -import '../veilid_support.dart'; +import '../dht_support/dht_support.dart'; part 'identity.freezed.dart'; part 'identity.g.dart'; diff --git a/lib/veilid_support/src/identity.freezed.dart b/packages/veilid_support/lib/src/identity.freezed.dart similarity index 100% rename from lib/veilid_support/src/identity.freezed.dart rename to packages/veilid_support/lib/src/identity.freezed.dart diff --git a/lib/veilid_support/src/identity.g.dart b/packages/veilid_support/lib/src/identity.g.dart similarity index 62% rename from lib/veilid_support/src/identity.g.dart rename to packages/veilid_support/lib/src/identity.g.dart index 616477a..7d3687e 100644 --- a/lib/veilid_support/src/identity.g.dart +++ b/packages/veilid_support/lib/src/identity.g.dart @@ -9,19 +9,19 @@ part of 'identity.dart'; _$AccountRecordInfoImpl _$$AccountRecordInfoImplFromJson( Map json) => _$AccountRecordInfoImpl( - accountRecord: OwnedDHTRecordPointer.fromJson(json['account_record']), + accountRecord: OwnedDHTRecordPointer.fromJson(json['accountRecord']), ); Map _$$AccountRecordInfoImplToJson( _$AccountRecordInfoImpl instance) => { - 'account_record': instance.accountRecord.toJson(), + 'accountRecord': instance.accountRecord, }; _$IdentityImpl _$$IdentityImplFromJson(Map json) => _$IdentityImpl( accountRecords: IMap>.fromJson( - json['account_records'] as Map, + json['accountRecords'] as Map, (value) => value as String, (value) => ISet.fromJson( value, (value) => AccountRecordInfo.fromJson(value))), @@ -29,10 +29,10 @@ _$IdentityImpl _$$IdentityImplFromJson(Map json) => Map _$$IdentityImplToJson(_$IdentityImpl instance) => { - 'account_records': instance.accountRecords.toJson( + 'accountRecords': instance.accountRecords.toJson( (value) => value, (value) => value.toJson( - (value) => value.toJson(), + (value) => value, ), ), }; @@ -40,24 +40,24 @@ Map _$$IdentityImplToJson(_$IdentityImpl instance) => _$IdentityMasterImpl _$$IdentityMasterImplFromJson(Map json) => _$IdentityMasterImpl( identityRecordKey: - Typed.fromJson(json['identity_record_key']), + Typed.fromJson(json['identityRecordKey']), identityPublicKey: - FixedEncodedString43.fromJson(json['identity_public_key']), + FixedEncodedString43.fromJson(json['identityPublicKey']), masterRecordKey: - Typed.fromJson(json['master_record_key']), - masterPublicKey: FixedEncodedString43.fromJson(json['master_public_key']), + Typed.fromJson(json['masterRecordKey']), + masterPublicKey: FixedEncodedString43.fromJson(json['masterPublicKey']), identitySignature: - FixedEncodedString86.fromJson(json['identity_signature']), - masterSignature: FixedEncodedString86.fromJson(json['master_signature']), + FixedEncodedString86.fromJson(json['identitySignature']), + masterSignature: FixedEncodedString86.fromJson(json['masterSignature']), ); Map _$$IdentityMasterImplToJson( _$IdentityMasterImpl instance) => { - 'identity_record_key': instance.identityRecordKey.toJson(), - 'identity_public_key': instance.identityPublicKey.toJson(), - 'master_record_key': instance.masterRecordKey.toJson(), - 'master_public_key': instance.masterPublicKey.toJson(), - 'identity_signature': instance.identitySignature.toJson(), - 'master_signature': instance.masterSignature.toJson(), + 'identityRecordKey': instance.identityRecordKey, + 'identityPublicKey': instance.identityPublicKey, + 'masterRecordKey': instance.masterRecordKey, + 'masterPublicKey': instance.masterPublicKey, + 'identitySignature': instance.identitySignature, + 'masterSignature': instance.masterSignature, }; diff --git a/lib/veilid_support/src/json_tools.dart b/packages/veilid_support/lib/src/json_tools.dart similarity index 100% rename from lib/veilid_support/src/json_tools.dart rename to packages/veilid_support/lib/src/json_tools.dart diff --git a/lib/veilid_support/src/protobuf_tools.dart b/packages/veilid_support/lib/src/protobuf_tools.dart similarity index 100% rename from lib/veilid_support/src/protobuf_tools.dart rename to packages/veilid_support/lib/src/protobuf_tools.dart diff --git a/lib/veilid_support/src/table_db.dart b/packages/veilid_support/lib/src/table_db.dart similarity index 100% rename from lib/veilid_support/src/table_db.dart rename to packages/veilid_support/lib/src/table_db.dart diff --git a/lib/veilid_support/src/veilid_log.dart b/packages/veilid_support/lib/src/veilid_log.dart similarity index 94% rename from lib/veilid_support/src/veilid_log.dart rename to packages/veilid_support/lib/src/veilid_log.dart index 8a343eb..b112292 100644 --- a/lib/veilid_support/src/veilid_log.dart +++ b/packages/veilid_support/lib/src/veilid_log.dart @@ -1,4 +1,3 @@ -import 'package:flutter/foundation.dart'; import 'package:loggy/loggy.dart'; import 'package:veilid/veilid.dart'; @@ -68,14 +67,14 @@ Future processLog(VeilidLog log) async { } } -void initVeilidLog() { +void initVeilidLog(bool debugMode) { // ignore: do_not_use_environment const isTrace = String.fromEnvironment('LOG_TRACE') != ''; LogLevel logLevel; if (isTrace) { logLevel = traceLevel; } else { - logLevel = kDebugMode ? LogLevel.debug : LogLevel.info; + logLevel = debugMode ? LogLevel.debug : LogLevel.info; } setVeilidLogLevel(logLevel); } diff --git a/lib/veilid_support/veilid_support.dart b/packages/veilid_support/lib/veilid_support.dart similarity index 100% rename from lib/veilid_support/veilid_support.dart rename to packages/veilid_support/lib/veilid_support.dart diff --git a/packages/veilid_support/pubspec.lock b/packages/veilid_support/pubspec.lock new file mode 100644 index 0000000..cba74d2 --- /dev/null +++ b/packages/veilid_support/pubspec.lock @@ -0,0 +1,780 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + _fe_analyzer_shared: + dependency: transitive + description: + name: _fe_analyzer_shared + sha256: eb376e9acf6938204f90eb3b1f00b578640d3188b4c8a8ec054f9f479af8d051 + url: "https://pub.dev" + source: hosted + version: "64.0.0" + analyzer: + dependency: transitive + description: + name: analyzer + sha256: "69f54f967773f6c26c7dcb13e93d7ccee8b17a641689da39e878d5cf13b06893" + url: "https://pub.dev" + source: hosted + version: "6.2.0" + args: + dependency: transitive + description: + name: args + sha256: eef6c46b622e0494a36c5a12d10d77fb4e855501a91c1b9ef9339326e58f0596 + url: "https://pub.dev" + source: hosted + version: "2.4.2" + async: + dependency: transitive + description: + name: async + sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" + url: "https://pub.dev" + source: hosted + version: "2.11.0" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" + url: "https://pub.dev" + source: hosted + version: "2.1.1" + build: + dependency: transitive + description: + name: build + sha256: "80184af8b6cb3e5c1c4ec6d8544d27711700bc3e6d2efad04238c7b5290889f0" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + build_config: + dependency: transitive + description: + name: build_config + sha256: bf80fcfb46a29945b423bd9aad884590fb1dc69b330a4d4700cac476af1708d1 + url: "https://pub.dev" + source: hosted + version: "1.1.1" + build_daemon: + dependency: transitive + description: + name: build_daemon + sha256: "0343061a33da9c5810b2d6cee51945127d8f4c060b7fbdd9d54917f0a3feaaa1" + url: "https://pub.dev" + source: hosted + version: "4.0.1" + build_resolvers: + dependency: transitive + description: + name: build_resolvers + sha256: "339086358431fa15d7eca8b6a36e5d783728cf025e559b834f4609a1fcfb7b0a" + url: "https://pub.dev" + source: hosted + version: "2.4.2" + build_runner: + dependency: "direct dev" + description: + name: build_runner + sha256: "67d591d602906ef9201caf93452495ad1812bea2074f04e25dbd7c133785821b" + url: "https://pub.dev" + source: hosted + version: "2.4.7" + build_runner_core: + dependency: transitive + description: + name: build_runner_core + sha256: c9e32d21dd6626b5c163d48b037ce906bbe428bc23ab77bcd77bb21e593b6185 + url: "https://pub.dev" + source: hosted + version: "7.2.11" + built_collection: + dependency: transitive + description: + name: built_collection + sha256: "376e3dd27b51ea877c28d525560790aee2e6fbb5f20e2f85d5081027d94e2100" + url: "https://pub.dev" + source: hosted + version: "5.1.1" + built_value: + dependency: transitive + description: + name: built_value + sha256: c9aabae0718ec394e5bc3c7272e6bb0dc0b32201a08fe185ec1d8401d3e39309 + url: "https://pub.dev" + source: hosted + version: "8.8.1" + change_case: + dependency: transitive + description: + name: change_case + sha256: f4e08feaa845e75e4f5ad2b0e15f24813d7ea6c27e7b78252f0c17f752cf1157 + url: "https://pub.dev" + source: hosted + version: "1.1.0" + characters: + dependency: transitive + description: + name: characters + sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" + url: "https://pub.dev" + source: hosted + version: "1.3.0" + charcode: + dependency: transitive + description: + name: charcode + sha256: fb98c0f6d12c920a02ee2d998da788bca066ca5f148492b7085ee23372b12306 + url: "https://pub.dev" + source: hosted + version: "1.3.1" + checked_yaml: + dependency: transitive + description: + name: checked_yaml + sha256: feb6bed21949061731a7a75fc5d2aa727cf160b91af9a3e464c5e3a32e28b5ff + url: "https://pub.dev" + source: hosted + version: "2.0.3" + code_builder: + dependency: transitive + description: + name: code_builder + sha256: feee43a5c05e7b3199bb375a86430b8ada1b04104f2923d0e03cc01ca87b6d84 + url: "https://pub.dev" + source: hosted + version: "4.9.0" + collection: + dependency: transitive + description: + name: collection + sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a + url: "https://pub.dev" + source: hosted + version: "1.18.0" + convert: + dependency: transitive + description: + name: convert + sha256: "0f08b14755d163f6e2134cb58222dd25ea2a2ee8a195e53983d57c075324d592" + url: "https://pub.dev" + source: hosted + version: "3.1.1" + coverage: + dependency: transitive + description: + name: coverage + sha256: "8acabb8306b57a409bf4c83522065672ee13179297a6bb0cb9ead73948df7c76" + url: "https://pub.dev" + source: hosted + version: "1.7.2" + crypto: + dependency: transitive + description: + name: crypto + sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab + url: "https://pub.dev" + source: hosted + version: "3.0.3" + dart_style: + dependency: transitive + description: + name: dart_style + sha256: "40ae61a5d43feea6d24bd22c0537a6629db858963b99b4bc1c3db80676f32368" + url: "https://pub.dev" + source: hosted + version: "2.3.4" + equatable: + dependency: transitive + description: + name: equatable + sha256: c2b87cb7756efdf69892005af546c56c0b5037f54d2a88269b4f347a505e3ca2 + url: "https://pub.dev" + source: hosted + version: "2.0.5" + fast_immutable_collections: + dependency: "direct main" + description: + name: fast_immutable_collections + sha256: "3eb1d7495c70598964add20e10666003fad6e855b108fe684ebcbf8ad0c8e120" + url: "https://pub.dev" + source: hosted + version: "9.2.0" + ffi: + dependency: transitive + description: + name: ffi + sha256: "7bf0adc28a23d395f19f3f1eb21dd7cfd1dd9f8e1c50051c069122e6853bc878" + url: "https://pub.dev" + source: hosted + version: "2.1.0" + file: + dependency: transitive + description: + name: file + sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c" + url: "https://pub.dev" + source: hosted + version: "7.0.0" + file_utils: + dependency: transitive + description: + name: file_utils + sha256: d1e64389a22649095c8405c9e177272caf05139255931c9ff30d53b5c9bcaa34 + url: "https://pub.dev" + source: hosted + version: "1.0.1" + fixnum: + dependency: transitive + description: + name: fixnum + sha256: "25517a4deb0c03aa0f32fd12db525856438902d9c16536311e76cdc57b31d7d1" + url: "https://pub.dev" + source: hosted + version: "1.1.0" + flutter: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + flutter_web_plugins: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + freezed: + dependency: "direct dev" + description: + name: freezed + sha256: "6c5031daae12c7072b3a87eff98983076434b4889ef2a44384d0cae3f82372ba" + url: "https://pub.dev" + source: hosted + version: "2.4.6" + freezed_annotation: + dependency: "direct main" + description: + name: freezed_annotation + sha256: c3fd9336eb55a38cc1bbd79ab17573113a8deccd0ecbbf926cca3c62803b5c2d + url: "https://pub.dev" + source: hosted + version: "2.4.1" + frontend_server_client: + dependency: transitive + description: + name: frontend_server_client + sha256: "408e3ca148b31c20282ad6f37ebfa6f4bdc8fede5b74bc2f08d9d92b55db3612" + url: "https://pub.dev" + source: hosted + version: "3.2.0" + glob: + dependency: transitive + description: + name: glob + sha256: "0e7014b3b7d4dac1ca4d6114f82bf1782ee86745b9b42a92c9289c23d8a0ab63" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + globbing: + dependency: transitive + description: + name: globbing + sha256: "4f89cfaf6fa74c9c1740a96259da06bd45411ede56744e28017cc534a12b6e2d" + url: "https://pub.dev" + source: hosted + version: "1.0.0" + graphs: + dependency: transitive + description: + name: graphs + sha256: aedc5a15e78fc65a6e23bcd927f24c64dd995062bcd1ca6eda65a3cff92a4d19 + url: "https://pub.dev" + source: hosted + version: "2.3.1" + http_multi_server: + dependency: transitive + description: + name: http_multi_server + sha256: "97486f20f9c2f7be8f514851703d0119c3596d14ea63227af6f7a481ef2b2f8b" + url: "https://pub.dev" + source: hosted + version: "3.2.1" + http_parser: + dependency: transitive + description: + name: http_parser + sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b" + url: "https://pub.dev" + source: hosted + version: "4.0.2" + io: + dependency: transitive + description: + name: io + sha256: "2ec25704aba361659e10e3e5f5d672068d332fc8ac516421d483a11e5cbd061e" + url: "https://pub.dev" + source: hosted + version: "1.0.4" + js: + dependency: transitive + description: + name: js + sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3 + url: "https://pub.dev" + source: hosted + version: "0.6.7" + json_annotation: + dependency: "direct main" + description: + name: json_annotation + sha256: b10a7b2ff83d83c777edba3c6a0f97045ddadd56c944e1a23a3fdf43a1bf4467 + url: "https://pub.dev" + source: hosted + version: "4.8.1" + json_serializable: + dependency: "direct dev" + description: + name: json_serializable + sha256: aa1f5a8912615733e0fdc7a02af03308933c93235bdc8d50d0b0c8a8ccb0b969 + url: "https://pub.dev" + source: hosted + version: "6.7.1" + lint_hard: + dependency: "direct dev" + description: + name: lint_hard + sha256: "44d15ec309b1a8e1aff99069df9dcb1597f49d5f588f32811ca28fb7b38c32fe" + url: "https://pub.dev" + source: hosted + version: "4.0.0" + logging: + dependency: transitive + description: + name: logging + sha256: "623a88c9594aa774443aa3eb2d41807a48486b5613e67599fb4c41c0ad47c340" + url: "https://pub.dev" + source: hosted + version: "1.2.0" + loggy: + dependency: "direct main" + description: + name: loggy + sha256: "981e03162bbd3a5a843026f75f73d26e4a0d8aa035ae060456ca7b30dfd1e339" + url: "https://pub.dev" + source: hosted + version: "2.0.3" + matcher: + dependency: transitive + description: + name: matcher + sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb + url: "https://pub.dev" + source: hosted + version: "0.12.16+1" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + sha256: "9528f2f296073ff54cb9fee677df673ace1218163c3bc7628093e7eed5203d41" + url: "https://pub.dev" + source: hosted + version: "0.5.0" + meta: + dependency: transitive + description: + name: meta + sha256: a6e590c838b18133bb482a2745ad77c5bb7715fb0451209e1a7567d416678b8e + url: "https://pub.dev" + source: hosted + version: "1.10.0" + mime: + dependency: transitive + description: + name: mime + sha256: e4ff8e8564c03f255408decd16e7899da1733852a9110a58fe6d1b817684a63e + url: "https://pub.dev" + source: hosted + version: "1.0.4" + mutex: + dependency: "direct main" + description: + name: mutex + sha256: "8827da25de792088eb33e572115a5eb0d61d61a3c01acbc8bcbe76ed78f1a1f2" + url: "https://pub.dev" + source: hosted + version: "3.1.0" + node_preamble: + dependency: transitive + description: + name: node_preamble + sha256: "6e7eac89047ab8a8d26cf16127b5ed26de65209847630400f9aefd7cd5c730db" + url: "https://pub.dev" + source: hosted + version: "2.0.2" + package_config: + dependency: transitive + description: + name: package_config + sha256: "1c5b77ccc91e4823a5af61ee74e6b972db1ef98c2ff5a18d3161c982a55448bd" + url: "https://pub.dev" + source: hosted + version: "2.1.0" + path: + dependency: transitive + description: + name: path + sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" + url: "https://pub.dev" + source: hosted + version: "1.9.0" + path_provider: + dependency: transitive + description: + name: path_provider + sha256: a1aa8aaa2542a6bc57e381f132af822420216c80d4781f7aa085ca3229208aaa + url: "https://pub.dev" + source: hosted + version: "2.1.1" + path_provider_android: + dependency: transitive + description: + name: path_provider_android + sha256: "477184d672607c0a3bf68fbbf601805f92ef79c82b64b4d6eb318cbca4c48668" + url: "https://pub.dev" + source: hosted + version: "2.2.2" + path_provider_foundation: + dependency: transitive + description: + name: path_provider_foundation + sha256: "19314d595120f82aca0ba62787d58dde2cc6b5df7d2f0daf72489e38d1b57f2d" + url: "https://pub.dev" + source: hosted + version: "2.3.1" + path_provider_linux: + dependency: transitive + description: + name: path_provider_linux + sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 + url: "https://pub.dev" + source: hosted + version: "2.2.1" + path_provider_platform_interface: + dependency: transitive + description: + name: path_provider_platform_interface + sha256: "94b1e0dd80970c1ce43d5d4e050a9918fce4f4a775e6142424c30a29a363265c" + url: "https://pub.dev" + source: hosted + version: "2.1.1" + path_provider_windows: + dependency: transitive + description: + name: path_provider_windows + sha256: "8bc9f22eee8690981c22aa7fc602f5c85b497a6fb2ceb35ee5a5e5ed85ad8170" + url: "https://pub.dev" + source: hosted + version: "2.2.1" + platform: + dependency: transitive + description: + name: platform + sha256: "0a279f0707af40c890e80b1e9df8bb761694c074ba7e1d4ab1bc4b728e200b59" + url: "https://pub.dev" + source: hosted + version: "3.1.3" + plugin_platform_interface: + dependency: transitive + description: + name: plugin_platform_interface + sha256: f4f88d4a900933e7267e2b353594774fc0d07fb072b47eedcd5b54e1ea3269f8 + url: "https://pub.dev" + source: hosted + version: "2.1.7" + pool: + dependency: transitive + description: + name: pool + sha256: "20fe868b6314b322ea036ba325e6fc0711a22948856475e2c2b6306e8ab39c2a" + url: "https://pub.dev" + source: hosted + version: "1.5.1" + protobuf: + dependency: "direct main" + description: + name: protobuf + sha256: "68645b24e0716782e58948f8467fd42a880f255096a821f9e7d0ec625b00c84d" + url: "https://pub.dev" + source: hosted + version: "3.1.0" + pub_semver: + dependency: transitive + description: + name: pub_semver + sha256: "40d3ab1bbd474c4c2328c91e3a7df8c6dd629b79ece4c4bd04bee496a224fb0c" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + pubspec_parse: + dependency: transitive + description: + name: pubspec_parse + sha256: c63b2876e58e194e4b0828fcb080ad0e06d051cb607a6be51a9e084f47cb9367 + url: "https://pub.dev" + source: hosted + version: "1.2.3" + shelf: + dependency: transitive + description: + name: shelf + sha256: ad29c505aee705f41a4d8963641f91ac4cee3c8fad5947e033390a7bd8180fa4 + url: "https://pub.dev" + source: hosted + version: "1.4.1" + shelf_packages_handler: + dependency: transitive + description: + name: shelf_packages_handler + sha256: "89f967eca29607c933ba9571d838be31d67f53f6e4ee15147d5dc2934fee1b1e" + url: "https://pub.dev" + source: hosted + version: "3.0.2" + shelf_static: + dependency: transitive + description: + name: shelf_static + sha256: a41d3f53c4adf0f57480578c1d61d90342cd617de7fc8077b1304643c2d85c1e + url: "https://pub.dev" + source: hosted + version: "1.1.2" + shelf_web_socket: + dependency: transitive + description: + name: shelf_web_socket + sha256: "9ca081be41c60190ebcb4766b2486a7d50261db7bd0f5d9615f2d653637a84c1" + url: "https://pub.dev" + source: hosted + version: "1.0.4" + sky_engine: + dependency: transitive + description: flutter + source: sdk + version: "0.0.99" + source_gen: + dependency: transitive + description: + name: source_gen + sha256: "14658ba5f669685cd3d63701d01b31ea748310f7ab854e471962670abcf57832" + url: "https://pub.dev" + source: hosted + version: "1.5.0" + source_helper: + dependency: transitive + description: + name: source_helper + sha256: "6adebc0006c37dd63fe05bca0a929b99f06402fc95aa35bf36d67f5c06de01fd" + url: "https://pub.dev" + source: hosted + version: "1.3.4" + source_map_stack_trace: + dependency: transitive + description: + name: source_map_stack_trace + sha256: "84cf769ad83aa6bb61e0aa5a18e53aea683395f196a6f39c4c881fb90ed4f7ae" + url: "https://pub.dev" + source: hosted + version: "2.1.1" + source_maps: + dependency: transitive + description: + name: source_maps + sha256: "708b3f6b97248e5781f493b765c3337db11c5d2c81c3094f10904bfa8004c703" + url: "https://pub.dev" + source: hosted + version: "0.10.12" + source_span: + dependency: transitive + description: + name: source_span + sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" + url: "https://pub.dev" + source: hosted + version: "1.10.0" + stack_trace: + dependency: transitive + description: + name: stack_trace + sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" + url: "https://pub.dev" + source: hosted + version: "1.11.1" + stream_channel: + dependency: transitive + description: + name: stream_channel + sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 + url: "https://pub.dev" + source: hosted + version: "2.1.2" + stream_transform: + dependency: transitive + description: + name: stream_transform + sha256: "14a00e794c7c11aa145a170587321aedce29769c08d7f58b1d141da75e3b1c6f" + url: "https://pub.dev" + source: hosted + version: "2.1.0" + string_scanner: + dependency: transitive + description: + name: string_scanner + sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" + url: "https://pub.dev" + source: hosted + version: "1.2.0" + system_info2: + dependency: transitive + description: + name: system_info2 + sha256: af2f948e3f31a3367a049932a8ad59faf0063ecf836a020d975b9f41566d8bc9 + url: "https://pub.dev" + source: hosted + version: "3.0.2" + system_info_plus: + dependency: transitive + description: + name: system_info_plus + sha256: b915c811c6605b802f3988859bc2bb79c95f735762a75b5451741f7a2b949d1b + url: "https://pub.dev" + source: hosted + version: "0.0.5" + term_glyph: + dependency: transitive + description: + name: term_glyph + sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 + url: "https://pub.dev" + source: hosted + version: "1.2.1" + test: + dependency: "direct dev" + description: + name: test + sha256: "3d028996109ad5c253674c7f347822fb994a087614d6f353e6039704b4661ff2" + url: "https://pub.dev" + source: hosted + version: "1.25.0" + test_api: + dependency: transitive + description: + name: test_api + sha256: "9955ae474176f7ac8ee4e989dadfb411a58c30415bcfb648fa04b2b8a03afa7f" + url: "https://pub.dev" + source: hosted + version: "0.7.0" + test_core: + dependency: transitive + description: + name: test_core + sha256: "2bc4b4ecddd75309300d8096f781c0e3280ca1ef85beda558d33fcbedc2eead4" + url: "https://pub.dev" + source: hosted + version: "0.6.0" + timing: + dependency: transitive + description: + name: timing + sha256: "70a3b636575d4163c477e6de42f247a23b315ae20e86442bebe32d3cabf61c32" + url: "https://pub.dev" + source: hosted + version: "1.0.1" + typed_data: + dependency: transitive + description: + name: typed_data + sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c + url: "https://pub.dev" + source: hosted + version: "1.3.2" + vector_math: + dependency: transitive + description: + name: vector_math + sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + veilid: + dependency: "direct main" + description: + path: "../../../veilid/veilid-flutter" + relative: true + source: path + version: "0.2.5" + vm_service: + dependency: transitive + description: + name: vm_service + sha256: a2662fb1f114f4296cf3f5a50786a2d888268d7776cf681aa17d660ffa23b246 + url: "https://pub.dev" + source: hosted + version: "14.0.0" + watcher: + dependency: transitive + description: + name: watcher + sha256: "3d2ad6751b3c16cf07c7fca317a1413b3f26530319181b37e3b9039b84fc01d8" + url: "https://pub.dev" + source: hosted + version: "1.1.0" + web: + dependency: transitive + description: + name: web + sha256: afe077240a270dcfd2aafe77602b4113645af95d0ad31128cc02bce5ac5d5152 + url: "https://pub.dev" + source: hosted + version: "0.3.0" + web_socket_channel: + dependency: transitive + description: + name: web_socket_channel + sha256: d88238e5eac9a42bb43ca4e721edba3c08c6354d4a53063afaa568516217621b + url: "https://pub.dev" + source: hosted + version: "2.4.0" + webkit_inspection_protocol: + dependency: transitive + description: + name: webkit_inspection_protocol + sha256: "87d3f2333bb240704cd3f1c6b5b7acd8a10e7f0bc28c28dcf14e782014f4a572" + url: "https://pub.dev" + source: hosted + version: "1.2.1" + win32: + dependency: transitive + description: + name: win32 + sha256: b0f37db61ba2f2e9b7a78a1caece0052564d1bc70668156cf3a29d676fe4e574 + url: "https://pub.dev" + source: hosted + version: "5.1.1" + xdg_directories: + dependency: transitive + description: + name: xdg_directories + sha256: "589ada45ba9e39405c198fe34eb0f607cddb2108527e658136120892beac46d2" + url: "https://pub.dev" + source: hosted + version: "1.0.3" + yaml: + dependency: transitive + description: + name: yaml + sha256: "75769501ea3489fca56601ff33454fe45507ea3bfb014161abc3b43ae25989d5" + url: "https://pub.dev" + source: hosted + version: "3.1.2" +sdks: + dart: ">=3.2.0-194.0.dev <4.0.0" + flutter: ">=3.10.6" diff --git a/packages/veilid_support/pubspec.yaml b/packages/veilid_support/pubspec.yaml new file mode 100644 index 0000000..397b24b --- /dev/null +++ b/packages/veilid_support/pubspec.yaml @@ -0,0 +1,26 @@ +name: veilid_support +description: Veilid Support Library +publish_to: 'none' +version: 1.0.2+0 + +environment: + sdk: '>=3.0.5 <4.0.0' + flutter: ">=3.10.0" + +dependencies: + fast_immutable_collections: ^9.1.5 + freezed_annotation: ^2.2.0 + json_annotation: ^4.8.1 + loggy: ^2.0.3 + mutex: ^3.1.0 + protobuf: ^3.0.0 + veilid: + # veilid: ^0.0.1 + path: ../../../veilid/veilid-flutter + +dev_dependencies: + build_runner: ^2.4.6 + test: ^1.25.0 + freezed: ^2.3.5 + json_serializable: ^6.7.1 + lint_hard: ^4.0.0 diff --git a/pubspec.lock b/pubspec.lock index 72af3e7..036283a 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -21,10 +21,10 @@ packages: dependency: "direct main" description: name: animated_theme_switcher - sha256: de8ce9872d6e6676ab1140f76ff00cd0084a9dfee62168044062629927949652 + sha256: "24ccd74437b8db78f6d1ec701804702817bced5f925b1b3419c7a93071e3d3e9" url: "https://pub.dev" source: hosted - version: "2.0.9" + version: "2.0.10" ansicolor: dependency: "direct main" description: @@ -61,10 +61,10 @@ packages: dependency: "direct main" description: name: awesome_extensions - sha256: "79bb5a24f1224795e599c75ec047ca6f4718e17be535544350d213bb37bc88cd" + sha256: a8b68d567119b9be85bc62d8dc2ab6712d74c0130bdc31a52c53d1058c4fe33a url: "https://pub.dev" source: hosted - version: "2.0.10" + version: "2.0.11" badges: dependency: "direct main" description: @@ -897,10 +897,10 @@ packages: dependency: transitive description: name: path_provider_android - sha256: e595b98692943b4881b219f0a9e3945118d3c16bd7e2813f98ec6e532d905f72 + sha256: "477184d672607c0a3bf68fbbf601805f92ef79c82b64b4d6eb318cbca4c48668" url: "https://pub.dev" source: hosted - version: "2.2.1" + version: "2.2.2" path_provider_foundation: dependency: transitive description: @@ -1505,6 +1505,13 @@ packages: relative: true source: path version: "0.2.5" + veilid_support: + dependency: "direct main" + description: + path: "packages/veilid_support" + relative: true + source: path + version: "1.0.2+0" visibility_detector: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index b061813..fc88a54 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -75,6 +75,8 @@ dependencies: veilid: # veilid: ^0.0.1 path: ../veilid/veilid-flutter + veilid_support: + path: packages/veilid_support window_manager: ^0.3.5 xterm: ^3.5.0 zxing2: ^0.2.0 From 31f562119a3701546001af346ddb162afea3f461 Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Thu, 4 Jan 2024 22:29:43 -0500 Subject: [PATCH 06/68] checkpoint --- build.sh | 5 + .../local_accounts_cubit.dart | 2 + .../local_accounts_state.dart | 11 + .../user_logins_cubit/user_logins_cubit.dart | 2 + .../user_logins_cubit/user_logins_state.dart | 11 + .../encryption_key_type.dart | 2 +- .../local_account}/local_account.dart | 2 +- .../local_account}/local_account.freezed.dart | 0 .../local_account}/local_account.g.dart | 0 lib/account_manager/models/models.dart | 4 + .../new_profile_spec.dart | 0 .../user_login}/user_login.dart | 0 .../user_login}/user_login.freezed.dart | 0 .../user_login}/user_login.g.dart | 0 .../account_repository.dart | 12 +- .../account_repository/active_logins.dart | 2 +- .../new_account_page/new_account_page.dart | 22 +- lib/init.dart | 36 +-- .../pages => layout}/chat_only.dart | 0 .../default_app_bar.dart | 0 .../pages => layout}/edit_account.dart | 0 .../pages => layout}/edit_contact.dart | 0 lib/layout/home.dart | 235 +++++++++++++++ .../pages => layout}/index.dart | 0 lib/layout/layout.dart | 1 + .../pages => layout}/main_pager/account.dart | 0 .../pages => layout}/main_pager/chats.dart | 0 .../main_pager/main_pager.dart | 0 .../pages => layout}/settings.dart | 0 .../components/signal_strength_meter.dart | 66 ----- lib/old_to_refactor/pages/home.dart | 269 ------------------ .../providers/connection_state.dart | 29 -- .../providers/connection_state.freezed.dart | 138 --------- lib/processor.dart | 103 ------- lib/router/cubit/router_cubit.dart | 5 +- lib/theme/models/models.dart | 4 + lib/theme/{ => models}/radix_generator.dart | 0 lib/theme/{ => models}/scale_color.dart | 0 lib/theme/{ => models}/scale_scheme.dart | 0 lib/theme/{ => models}/theme_preference.dart | 0 .../theme_preference.freezed.dart | 0 .../{ => models}/theme_preference.g.dart | 0 .../{ => repository}/theme_repository.dart | 3 +- lib/theme/theme.dart | 5 +- lib/tick.dart | 218 +++++++------- lib/tools/loggy.dart | 4 +- lib/tools/stream_wrapper_cubit.dart | 25 ++ lib/tools/tools.dart | 3 +- .../cubit/connection_state_cubit.dart | 12 + lib/veilid_processor/models/models.dart | 1 + .../models/processor_connection_state.dart | 19 ++ .../processor_connection_state.freezed.dart | 184 ++++++++++++ .../repository/processor_repository.dart | 132 +++++++++ lib/veilid_processor/veilid_processor.dart | 4 + .../views}/developer.dart | 6 +- .../views/signal_strength_meter.dart | 88 ++++++ lib/veilid_processor/views/views.dart | 2 + .../lib/dht_support/dht_support.dart | 1 + .../lib/dht_support/src/dht_record.dart | 25 +- .../lib/dht_support/src/dht_record_cubit.dart | 53 ++++ .../lib/dht_support/src/dht_record_pool.dart | 199 ++++++++++--- .../lib/dht_support/src/dht_short_array.dart | 16 +- .../lib/src}/async_table_db_backed_cubit.dart | 5 +- .../veilid_support/lib/src}/async_value.dart | 0 .../lib/src}/async_value.freezed.dart | 0 packages/veilid_support/lib/src/identity.dart | 10 +- .../veilid_support/lib/src/veilid_log.dart | 2 +- .../veilid_support/lib/veilid_support.dart | 1 + packages/veilid_support/pubspec.lock | 8 + packages/veilid_support/pubspec.yaml | 4 +- 70 files changed, 1174 insertions(+), 817 deletions(-) rename lib/account_manager/{repository/account_repository => models}/encryption_key_type.dart (98%) rename lib/account_manager/{repository/account_repository => models/local_account}/local_account.dart (96%) rename lib/account_manager/{repository/account_repository => models/local_account}/local_account.freezed.dart (100%) rename lib/account_manager/{repository/account_repository => models/local_account}/local_account.g.dart (100%) create mode 100644 lib/account_manager/models/models.dart rename lib/account_manager/{repository/account_repository => models}/new_profile_spec.dart (100%) rename lib/account_manager/{repository/account_repository => models/user_login}/user_login.dart (100%) rename lib/account_manager/{repository/account_repository => models/user_login}/user_login.freezed.dart (100%) rename lib/account_manager/{repository/account_repository => models/user_login}/user_login.g.dart (100%) rename lib/{old_to_refactor/pages => layout}/chat_only.dart (100%) rename lib/{old_to_refactor/components => layout}/default_app_bar.dart (100%) rename lib/{old_to_refactor/pages => layout}/edit_account.dart (100%) rename lib/{old_to_refactor/pages => layout}/edit_contact.dart (100%) create mode 100644 lib/layout/home.dart rename lib/{old_to_refactor/pages => layout}/index.dart (100%) create mode 100644 lib/layout/layout.dart rename lib/{old_to_refactor/pages => layout}/main_pager/account.dart (100%) rename lib/{old_to_refactor/pages => layout}/main_pager/chats.dart (100%) rename lib/{old_to_refactor/pages => layout}/main_pager/main_pager.dart (100%) rename lib/{old_to_refactor/pages => layout}/settings.dart (100%) delete mode 100644 lib/old_to_refactor/components/signal_strength_meter.dart delete mode 100644 lib/old_to_refactor/pages/home.dart delete mode 100644 lib/old_to_refactor/providers/connection_state.dart delete mode 100644 lib/old_to_refactor/providers/connection_state.freezed.dart delete mode 100644 lib/processor.dart create mode 100644 lib/theme/models/models.dart rename lib/theme/{ => models}/radix_generator.dart (100%) rename lib/theme/{ => models}/scale_color.dart (100%) rename lib/theme/{ => models}/scale_scheme.dart (100%) rename lib/theme/{ => models}/theme_preference.dart (100%) rename lib/theme/{ => models}/theme_preference.freezed.dart (100%) rename lib/theme/{ => models}/theme_preference.g.dart (100%) rename lib/theme/{ => repository}/theme_repository.dart (98%) create mode 100644 lib/tools/stream_wrapper_cubit.dart create mode 100644 lib/veilid_processor/cubit/connection_state_cubit.dart create mode 100644 lib/veilid_processor/models/models.dart create mode 100644 lib/veilid_processor/models/processor_connection_state.dart create mode 100644 lib/veilid_processor/models/processor_connection_state.freezed.dart create mode 100644 lib/veilid_processor/repository/processor_repository.dart create mode 100644 lib/veilid_processor/veilid_processor.dart rename lib/{old_to_refactor/pages => veilid_processor/views}/developer.dart (98%) create mode 100644 lib/veilid_processor/views/signal_strength_meter.dart create mode 100644 lib/veilid_processor/views/views.dart create mode 100644 packages/veilid_support/lib/dht_support/src/dht_record_cubit.dart rename {lib/tools => packages/veilid_support/lib/src}/async_table_db_backed_cubit.dart (89%) rename {lib/tools => packages/veilid_support/lib/src}/async_value.dart (100%) rename {lib/tools => packages/veilid_support/lib/src}/async_value.freezed.dart (100%) diff --git a/build.sh b/build.sh index 1a6cdf5..569fe28 100755 --- a/build.sh +++ b/build.sh @@ -1,5 +1,10 @@ #!/bin/bash set -e + +pushd packages/veilid_support > /dev/null +./build.sh +popd > /dev/null + dart run build_runner build --delete-conflicting-outputs pushd lib > /dev/null diff --git a/lib/account_manager/cubit/local_accounts_cubit/local_accounts_cubit.dart b/lib/account_manager/cubit/local_accounts_cubit/local_accounts_cubit.dart index 34fdccb..d885a7e 100644 --- a/lib/account_manager/cubit/local_accounts_cubit/local_accounts_cubit.dart +++ b/lib/account_manager/cubit/local_accounts_cubit/local_accounts_cubit.dart @@ -2,7 +2,9 @@ import 'dart:async'; import 'package:bloc/bloc.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; +import 'package:veilid_support/veilid_support.dart'; +import '../../models/models.dart'; import '../../repository/account_repository/account_repository.dart'; part 'local_accounts_state.dart'; diff --git a/lib/account_manager/cubit/local_accounts_cubit/local_accounts_state.dart b/lib/account_manager/cubit/local_accounts_cubit/local_accounts_state.dart index 13950c3..3b8d695 100644 --- a/lib/account_manager/cubit/local_accounts_cubit/local_accounts_state.dart +++ b/lib/account_manager/cubit/local_accounts_cubit/local_accounts_state.dart @@ -1,3 +1,14 @@ part of 'local_accounts_cubit.dart'; typedef LocalAccountsState = IList; + +extension LocalAccountsStateExt on LocalAccountsState { + LocalAccount? fetchLocalAccount({required TypedKey accountMasterRecordKey}) { + final idx = indexWhere( + (e) => e.identityMaster.masterRecordKey == accountMasterRecordKey); + if (idx == -1) { + return null; + } + return this[idx]; + } +} diff --git a/lib/account_manager/cubit/user_logins_cubit/user_logins_cubit.dart b/lib/account_manager/cubit/user_logins_cubit/user_logins_cubit.dart index e6ad92a..fd2f4ff 100644 --- a/lib/account_manager/cubit/user_logins_cubit/user_logins_cubit.dart +++ b/lib/account_manager/cubit/user_logins_cubit/user_logins_cubit.dart @@ -2,7 +2,9 @@ import 'dart:async'; import 'package:bloc/bloc.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; +import 'package:veilid_support/veilid_support.dart'; +import '../../models/models.dart'; import '../../repository/account_repository/account_repository.dart'; part 'user_logins_state.dart'; diff --git a/lib/account_manager/cubit/user_logins_cubit/user_logins_state.dart b/lib/account_manager/cubit/user_logins_cubit/user_logins_state.dart index 27dec5c..04c70d7 100644 --- a/lib/account_manager/cubit/user_logins_cubit/user_logins_state.dart +++ b/lib/account_manager/cubit/user_logins_cubit/user_logins_state.dart @@ -1,3 +1,14 @@ part of 'user_logins_cubit.dart'; typedef UserLoginsState = IList; + +extension UserLoginsStateExt on UserLoginsState { + UserLogin? fetchUserLogin({required TypedKey accountMasterRecordKey}) { + final idx = + indexWhere((e) => e.accountMasterRecordKey == accountMasterRecordKey); + if (idx == -1) { + return null; + } + return this[idx]; + } +} diff --git a/lib/account_manager/repository/account_repository/encryption_key_type.dart b/lib/account_manager/models/encryption_key_type.dart similarity index 98% rename from lib/account_manager/repository/account_repository/encryption_key_type.dart rename to lib/account_manager/models/encryption_key_type.dart index 2c00f27..22897b4 100644 --- a/lib/account_manager/repository/account_repository/encryption_key_type.dart +++ b/lib/account_manager/models/encryption_key_type.dart @@ -9,7 +9,7 @@ import 'dart:typed_data'; import 'package:change_case/change_case.dart'; import 'package:veilid_support/veilid_support.dart'; -import '../../../../proto/proto.dart' as proto; +import '../../../proto/proto.dart' as proto; enum EncryptionKeyType { none, diff --git a/lib/account_manager/repository/account_repository/local_account.dart b/lib/account_manager/models/local_account/local_account.dart similarity index 96% rename from lib/account_manager/repository/account_repository/local_account.dart rename to lib/account_manager/models/local_account/local_account.dart index eaf0fa8..1998961 100644 --- a/lib/account_manager/repository/account_repository/local_account.dart +++ b/lib/account_manager/models/local_account/local_account.dart @@ -3,7 +3,7 @@ import 'dart:typed_data'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:veilid_support/veilid_support.dart'; -import 'encryption_key_type.dart'; +import '../../models/encryption_key_type.dart'; part 'local_account.g.dart'; part 'local_account.freezed.dart'; diff --git a/lib/account_manager/repository/account_repository/local_account.freezed.dart b/lib/account_manager/models/local_account/local_account.freezed.dart similarity index 100% rename from lib/account_manager/repository/account_repository/local_account.freezed.dart rename to lib/account_manager/models/local_account/local_account.freezed.dart diff --git a/lib/account_manager/repository/account_repository/local_account.g.dart b/lib/account_manager/models/local_account/local_account.g.dart similarity index 100% rename from lib/account_manager/repository/account_repository/local_account.g.dart rename to lib/account_manager/models/local_account/local_account.g.dart diff --git a/lib/account_manager/models/models.dart b/lib/account_manager/models/models.dart new file mode 100644 index 0000000..320e917 --- /dev/null +++ b/lib/account_manager/models/models.dart @@ -0,0 +1,4 @@ +export 'encryption_key_type.dart'; +export 'local_account/local_account.dart'; +export 'new_profile_spec.dart'; +export 'user_login/user_login.dart'; diff --git a/lib/account_manager/repository/account_repository/new_profile_spec.dart b/lib/account_manager/models/new_profile_spec.dart similarity index 100% rename from lib/account_manager/repository/account_repository/new_profile_spec.dart rename to lib/account_manager/models/new_profile_spec.dart diff --git a/lib/account_manager/repository/account_repository/user_login.dart b/lib/account_manager/models/user_login/user_login.dart similarity index 100% rename from lib/account_manager/repository/account_repository/user_login.dart rename to lib/account_manager/models/user_login/user_login.dart diff --git a/lib/account_manager/repository/account_repository/user_login.freezed.dart b/lib/account_manager/models/user_login/user_login.freezed.dart similarity index 100% rename from lib/account_manager/repository/account_repository/user_login.freezed.dart rename to lib/account_manager/models/user_login/user_login.freezed.dart diff --git a/lib/account_manager/repository/account_repository/user_login.g.dart b/lib/account_manager/models/user_login/user_login.g.dart similarity index 100% rename from lib/account_manager/repository/account_repository/user_login.g.dart rename to lib/account_manager/models/user_login/user_login.g.dart diff --git a/lib/account_manager/repository/account_repository/account_repository.dart b/lib/account_manager/repository/account_repository/account_repository.dart index 0e42a55..2be5565 100644 --- a/lib/account_manager/repository/account_repository/account_repository.dart +++ b/lib/account_manager/repository/account_repository/account_repository.dart @@ -2,16 +2,8 @@ import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:veilid_support/veilid_support.dart'; import '../../../../proto/proto.dart' as proto; +import '../../models/models.dart'; import 'active_logins.dart'; -import 'encryption_key_type.dart'; -import 'local_account.dart'; -import 'new_profile_spec.dart'; -import 'user_login.dart'; - -export 'active_logins.dart'; -export 'encryption_key_type.dart'; -export 'local_account.dart'; -export 'user_login.dart'; const String veilidChatAccountKey = 'com.veilid.veilidchat'; @@ -73,7 +65,7 @@ class AccountRepository { return localAccounts[idx]; } - UserLogin? fetchLogin({required TypedKey accountMasterRecordKey}) { + UserLogin? fetchUserLogin({required TypedKey accountMasterRecordKey}) { final userLogins = _activeLogins.requireValue.userLogins; final idx = userLogins .indexWhere((e) => e.accountMasterRecordKey == accountMasterRecordKey); diff --git a/lib/account_manager/repository/account_repository/active_logins.dart b/lib/account_manager/repository/account_repository/active_logins.dart index 2fab41f..9d4e713 100644 --- a/lib/account_manager/repository/account_repository/active_logins.dart +++ b/lib/account_manager/repository/account_repository/active_logins.dart @@ -3,7 +3,7 @@ import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:veilid_support/veilid_support.dart'; -import 'user_login.dart'; +import '../../models/models.dart'; part 'active_logins.g.dart'; part 'active_logins.freezed.dart'; diff --git a/lib/account_manager/view/new_account_page/new_account_page.dart b/lib/account_manager/view/new_account_page/new_account_page.dart index acf3f8d..007bb53 100644 --- a/lib/account_manager/view/new_account_page/new_account_page.dart +++ b/lib/account_manager/view/new_account_page/new_account_page.dart @@ -5,12 +5,12 @@ import 'package:flutter_form_builder/flutter_form_builder.dart'; import 'package:flutter_translate/flutter_translate.dart'; import 'package:form_builder_validators/form_builder_validators.dart'; import 'package:go_router/go_router.dart'; -import 'package:veilid_support/veilid_support.dart'; -import '../../../components/default_app_bar.dart'; -import '../../../components/signal_strength_meter.dart'; -import '../../../entities/entities.dart'; +import '../../../layout/default_app_bar.dart'; import '../../../tools/tools.dart'; +import '../../../veilid_processor/veilid_processor.dart'; +import '../../account_manager.dart'; +import '../../models/models.dart'; class NewAccountPage extends StatefulWidget { const NewAccountPage({super.key}); @@ -95,8 +95,7 @@ class NewAccountPageState extends State { @override Widget build(BuildContext context) { - final displayModalHUD = - isInAsyncCall || !localAccounts.hasValue || !logins.hasValue; + final displayModalHUD = isInAsyncCall; return Scaffold( // resizeToAvoidBottomInset: false, @@ -116,7 +115,16 @@ class NewAccountPageState extends State { onSubmit: (formKey) async { FocusScope.of(context).unfocus(); try { - await createAccount(); + final name = + _formKey.currentState!.fields[formFieldName]!.value as String; + final pronouns = _formKey.currentState!.fields[formFieldPronouns]! + .value as String? ?? + ''; + final newProfileSpec = + NewProfileSpec(name: name, pronouns: pronouns); + + await AccountRepository.instance + .createMasterIdentity(newProfileSpec); } on Exception catch (e) { if (context.mounted) { await showErrorModal(context, translate('new_account_page.error'), diff --git a/lib/init.dart b/lib/init.dart index 7c0ac92..29c2db2 100644 --- a/lib/init.dart +++ b/lib/init.dart @@ -1,35 +1,29 @@ import 'dart:async'; -import 'app.dart'; -import 'local_account_manager/account_manager.dart'; -import 'processor.dart'; -import 'tools/tools.dart'; -import '../packages/veilid_support/veilid_support.dart'; +import 'package:flutter/foundation.dart'; +import 'package:veilid_support/veilid_support.dart'; -final Completer eventualVeilid = Completer(); -final Processor processor = Processor(); +import 'account_manager/account_manager.dart'; +import 'app.dart'; +import 'tools/tools.dart'; +import 'veilid_processor/veilid_processor.dart'; final Completer eventualInitialized = Completer(); // Initialize Veilid Future initializeVeilid() async { - // Ensure this runs only once - if (eventualVeilid.isCompleted) { - return; - } - // Init Veilid - Veilid.instance - .initializeVeilidCore(getDefaultVeilidPlatformConfig(VeilidChatApp.name)); + Veilid.instance.initializeVeilidCore( + getDefaultVeilidPlatformConfig(kIsWeb, VeilidChatApp.name)); // Veilid logging - initVeilidLog(); + initVeilidLog(kDebugMode); + + // DHT Record Pool + await DHTRecordPool.init(); // Startup Veilid - await processor.startup(); - - // Share the initialized veilid instance to the rest of the app - eventualVeilid.complete(Veilid.instance); + await ProcessorRepository.instance.startup(); } // Initialize repositories @@ -38,9 +32,9 @@ Future initializeRepositories() async { } Future initializeVeilidChat() async { - log.info("Initializing Veilid"); + log.info('Initializing Veilid'); await initializeVeilid(); - log.info("Initializing Repositories"); + log.info('Initializing Repositories'); await initializeRepositories(); eventualInitialized.complete(); diff --git a/lib/old_to_refactor/pages/chat_only.dart b/lib/layout/chat_only.dart similarity index 100% rename from lib/old_to_refactor/pages/chat_only.dart rename to lib/layout/chat_only.dart diff --git a/lib/old_to_refactor/components/default_app_bar.dart b/lib/layout/default_app_bar.dart similarity index 100% rename from lib/old_to_refactor/components/default_app_bar.dart rename to lib/layout/default_app_bar.dart diff --git a/lib/old_to_refactor/pages/edit_account.dart b/lib/layout/edit_account.dart similarity index 100% rename from lib/old_to_refactor/pages/edit_account.dart rename to lib/layout/edit_account.dart diff --git a/lib/old_to_refactor/pages/edit_contact.dart b/lib/layout/edit_contact.dart similarity index 100% rename from lib/old_to_refactor/pages/edit_contact.dart rename to lib/layout/edit_contact.dart diff --git a/lib/layout/home.dart b/lib/layout/home.dart new file mode 100644 index 0000000..a78ebb9 --- /dev/null +++ b/lib/layout/home.dart @@ -0,0 +1,235 @@ +import 'package:awesome_extensions/awesome_extensions.dart'; +import 'package:fast_immutable_collections/fast_immutable_collections.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_animate/flutter_animate.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_translate/flutter_translate.dart'; +import 'package:go_router/go_router.dart'; + +import '../account_manager/account_manager.dart'; +import '../../proto/proto.dart' as proto; +import '../theme/theme.dart'; +import '../tools/tools.dart'; +import 'main_pager/main_pager.dart'; + +class HomePage extends StatefulWidget { + const HomePage({super.key}); + + @override + HomePageState createState() => HomePageState(); + + static Widget buildChatComponent() { + final contactList = ref.watch(fetchContactListProvider).asData?.value ?? + const IListConst([]); + + final activeChat = ref.watch(activeChatStateProvider); + if (activeChat == null) { + return const EmptyChatWidget(); + } + + final activeAccountInfo = + ref.watch(fetchActiveAccountProvider).asData?.value; + if (activeAccountInfo == null) { + return const EmptyChatWidget(); + } + + final activeChatContactIdx = contactList.indexWhere( + (c) => + proto.TypedKeyProto.fromProto(c.remoteConversationRecordKey) == + activeChat, + ); + if (activeChatContactIdx == -1) { + ref.read(activeChatStateProvider.notifier).state = null; + return const EmptyChatWidget(); + } + final activeChatContact = contactList[activeChatContactIdx]; + + return ChatComponent( + activeAccountInfo: activeAccountInfo, + activeChat: activeChat, + activeChatContact: activeChatContact); + } +} + +class HomePageState extends State with TickerProviderStateMixin { + final _unfocusNode = FocusNode(); + + @override + void initState() { + super.initState(); + + WidgetsBinding.instance.addPostFrameCallback((_) async { + await changeWindowSetup( + TitleBarStyle.normal, OrientationCapability.normal); + }); + } + + @override + void dispose() { + _unfocusNode.dispose(); + super.dispose(); + } + + // ignore: prefer_expression_function_bodies + Widget buildAccountList() { + return const Column(children: [ + Center(child: Text('Small Profile')), + Center(child: Text('Contact invitations')), + Center(child: Text('Contacts')) + ]); + } + + Widget buildUnlockAccount( + BuildContext context, + IList localAccounts, + // ignore: prefer_expression_function_bodies + ) { + return const Center(child: Text('unlock account')); + } + + /// We have an active, unlocked, user login + Widget buildReadyAccount( + BuildContext context, + IList localAccounts, + TypedKey activeUserLogin, + proto.Account account) { + final theme = Theme.of(context); + final scale = theme.extension()!; + + return Column(children: [ + Row(children: [ + IconButton( + icon: const Icon(Icons.settings), + color: scale.secondaryScale.text, + constraints: const BoxConstraints.expand(height: 64, width: 64), + style: ButtonStyle( + backgroundColor: + MaterialStateProperty.all(scale.secondaryScale.border), + shape: MaterialStateProperty.all(const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(16))))), + tooltip: translate('app_bar.settings_tooltip'), + onPressed: () async { + context.go('/home/settings'); + }).paddingLTRB(0, 0, 8, 0), + ProfileWidget( + name: account.profile.name, + pronouns: account.profile.pronouns, + ).expanded(), + ]).paddingAll(8), + MainPager( + localAccounts: localAccounts, + activeUserLogin: activeUserLogin, + account: account) + .expanded() + ]); + } + + Widget buildUserPanel() => Builder(builder: (context) { + final activeUserLogin = context.watch().state; + final localAccounts = context.watch().state; + + if (activeUserLogin == null) { + // If no logged in user is active, show the loading panel + return waitingPage(context); + } + + final accountV = ref.watch( + fetchAccountProvider(accountMasterRecordKey: activeUserLogin)); + if (!accountV.hasValue) { + return waitingPage(context); + } + final account = accountV.requireValue; + switch (account.status) { + case AccountInfoStatus.noAccount: + Future.delayed(0.ms, () async { + await showErrorModal( + context, + translate('home.missing_account_title'), + translate('home.missing_account_text')); + // Delete account + await ref + .read(localAccountsProvider.notifier) + .deleteLocalAccount(activeUserLogin); + // Switch to no active user login + await ref.read(loginsProvider.notifier).switchToAccount(null); + }); + return waitingPage(context); + case AccountInfoStatus.accountInvalid: + Future.delayed(0.ms, () async { + await showErrorModal( + context, + translate('home.invalid_account_title'), + translate('home.invalid_account_text')); + // Delete account + await ref + .read(localAccountsProvider.notifier) + .deleteLocalAccount(activeUserLogin); + // Switch to no active user login + await ref.read(loginsProvider.notifier).switchToAccount(null); + }); + return waitingPage(context); + case AccountInfoStatus.accountLocked: + // Show unlock widget + return buildUnlockAccount(context, localAccounts); + case AccountInfoStatus.accountReady: + return buildReadyAccount( + context, + localAccounts, + activeUserLogin, + account.account!, + ); + } + }); + + Widget buildPhone() => + Material(color: Colors.transparent, child: buildUserPanel()); + + Widget buildTabletLeftPane() => + Material(color: Colors.transparent, child: buildUserPanel()); + + Widget buildTabletRightPane() => HomePage.buildChatComponent(); + + // ignore: prefer_expression_function_bodies + Widget buildTablet() => Builder(builder: (context) { + final w = MediaQuery.of(context).size.width; + final theme = Theme.of(context); + final scale = theme.extension()!; + + final children = [ + ConstrainedBox( + constraints: const BoxConstraints(minWidth: 300, maxWidth: 300), + child: ConstrainedBox( + constraints: BoxConstraints(maxWidth: w / 2), + child: buildTabletLeftPane())), + SizedBox( + width: 2, + height: double.infinity, + child: ColoredBox(color: scale.primaryScale.hoverBorder)), + Expanded(child: buildTabletRightPane()), + ]; + + return Row( + children: children, + ); + }); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final scale = theme.extension()!; + + return SafeArea( + child: GestureDetector( + onTap: () => FocusScope.of(context).requestFocus(_unfocusNode), + child: DecoratedBox( + decoration: BoxDecoration( + color: scale.primaryScale.activeElementBackground), + child: responsiveVisibility( + context: context, + phone: false, + ) + ? buildTablet() + : buildPhone(), + ))); + } +} diff --git a/lib/old_to_refactor/pages/index.dart b/lib/layout/index.dart similarity index 100% rename from lib/old_to_refactor/pages/index.dart rename to lib/layout/index.dart diff --git a/lib/layout/layout.dart b/lib/layout/layout.dart new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/lib/layout/layout.dart @@ -0,0 +1 @@ + diff --git a/lib/old_to_refactor/pages/main_pager/account.dart b/lib/layout/main_pager/account.dart similarity index 100% rename from lib/old_to_refactor/pages/main_pager/account.dart rename to lib/layout/main_pager/account.dart diff --git a/lib/old_to_refactor/pages/main_pager/chats.dart b/lib/layout/main_pager/chats.dart similarity index 100% rename from lib/old_to_refactor/pages/main_pager/chats.dart rename to lib/layout/main_pager/chats.dart diff --git a/lib/old_to_refactor/pages/main_pager/main_pager.dart b/lib/layout/main_pager/main_pager.dart similarity index 100% rename from lib/old_to_refactor/pages/main_pager/main_pager.dart rename to lib/layout/main_pager/main_pager.dart diff --git a/lib/old_to_refactor/pages/settings.dart b/lib/layout/settings.dart similarity index 100% rename from lib/old_to_refactor/pages/settings.dart rename to lib/layout/settings.dart diff --git a/lib/old_to_refactor/components/signal_strength_meter.dart b/lib/old_to_refactor/components/signal_strength_meter.dart deleted file mode 100644 index 2593515..0000000 --- a/lib/old_to_refactor/components/signal_strength_meter.dart +++ /dev/null @@ -1,66 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:signal_strength_indicator/signal_strength_indicator.dart'; -import 'package:veilid_support/veilid_support.dart'; - -import '../providers/connection_state.dart'; -import '../tools/tools.dart'; - -xxx move to feature level - -class SignalStrengthMeterWidget extends Widget { - const SignalStrengthMeterWidget({super.key}); - - @override - // ignore: prefer_expression_function_bodies - Widget build(BuildContext context, WidgetRef ref) { - final theme = Theme.of(context); - final scale = theme.extension()!; - - const iconSize = 16.0; - final connState = ref.watch(connectionStateProvider); - - late final double value; - late final Color color; - late final Color inactiveColor; - switch (connState.attachment.state) { - case AttachmentState.detached: - return Icon(Icons.signal_cellular_nodata, - size: iconSize, color: scale.grayScale.text); - case AttachmentState.detaching: - return Icon(Icons.signal_cellular_off, - size: iconSize, color: scale.grayScale.text); - case AttachmentState.attaching: - value = 0; - color = scale.primaryScale.text; - case AttachmentState.attachedWeak: - value = 1; - color = scale.primaryScale.text; - case AttachmentState.attachedStrong: - value = 2; - color = scale.primaryScale.text; - case AttachmentState.attachedGood: - value = 3; - color = scale.primaryScale.text; - case AttachmentState.fullyAttached: - value = 4; - color = scale.primaryScale.text; - case AttachmentState.overAttached: - value = 4; - color = scale.secondaryScale.subtleText; - } - inactiveColor = scale.grayScale.subtleText; - - return GestureDetector( - onLongPress: () async { - await context.push('/developer'); - }, - child: SignalStrengthIndicator.bars( - value: value, - activeColor: color, - inactiveColor: inactiveColor, - size: iconSize, - barCount: 4, - spacing: 1, - )); - } -} diff --git a/lib/old_to_refactor/pages/home.dart b/lib/old_to_refactor/pages/home.dart deleted file mode 100644 index 6284dfd..0000000 --- a/lib/old_to_refactor/pages/home.dart +++ /dev/null @@ -1,269 +0,0 @@ -import 'package:awesome_extensions/awesome_extensions.dart'; -import 'package:fast_immutable_collections/fast_immutable_collections.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_animate/flutter_animate.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:flutter_translate/flutter_translate.dart'; -import 'package:go_router/go_router.dart'; - -import '../../proto/proto.dart' as proto; -import '../../components/chat_component.dart'; -import '../../components/empty_chat_widget.dart'; -import '../../components/profile_widget.dart'; -import '../../entities/local_account.dart'; -import '../providers/account.dart'; -import '../providers/chat.dart'; -import '../providers/contact.dart'; -import '../../local_accounts/local_accounts.dart'; -import '../providers/logins.dart'; -import '../providers/window_control.dart'; -import '../../tools/tools.dart'; -import '../../../packages/veilid_support/veilid_support.dart'; -import 'main_pager/main_pager.dart'; - -class HomePage extends StatefulWidget { - const HomePage({super.key}); - - @override - HomePageState createState() => HomePageState(); - - static Widget buildChatComponent(BuildContext context, WidgetRef ref) { - final contactList = ref.watch(fetchContactListProvider).asData?.value ?? - const IListConst([]); - - final activeChat = ref.watch(activeChatStateProvider); - if (activeChat == null) { - return const EmptyChatWidget(); - } - - final activeAccountInfo = - ref.watch(fetchActiveAccountProvider).asData?.value; - if (activeAccountInfo == null) { - return const EmptyChatWidget(); - } - - final activeChatContactIdx = contactList.indexWhere( - (c) => - proto.TypedKeyProto.fromProto(c.remoteConversationRecordKey) == - activeChat, - ); - if (activeChatContactIdx == -1) { - ref.read(activeChatStateProvider.notifier).state = null; - return const EmptyChatWidget(); - } - final activeChatContact = contactList[activeChatContactIdx]; - - return ChatComponent( - activeAccountInfo: activeAccountInfo, - activeChat: activeChat, - activeChatContact: activeChatContact); - } -} - -class HomePageState extends ConsumerState - with TickerProviderStateMixin { - final _unfocusNode = FocusNode(); - - @override - void initState() { - super.initState(); - - WidgetsBinding.instance.addPostFrameCallback((_) async { - setState(() {}); - await ref.read(windowControlProvider.notifier).changeWindowSetup( - TitleBarStyle.normal, OrientationCapability.normal); - }); - } - - @override - void dispose() { - _unfocusNode.dispose(); - super.dispose(); - } - - // ignore: prefer_expression_function_bodies - Widget buildAccountList() { - return const Column(children: [ - Center(child: Text('Small Profile')), - Center(child: Text('Contact invitations')), - Center(child: Text('Contacts')) - ]); - } - - Widget buildUnlockAccount( - BuildContext context, - IList localAccounts, - // ignore: prefer_expression_function_bodies - ) { - return const Center(child: Text('unlock account')); - } - - /// We have an active, unlocked, user login - Widget buildReadyAccount( - BuildContext context, - IList localAccounts, - TypedKey activeUserLogin, - proto.Account account) { - final theme = Theme.of(context); - final scale = theme.extension()!; - - return Column(children: [ - Row(children: [ - IconButton( - icon: const Icon(Icons.settings), - color: scale.secondaryScale.text, - constraints: const BoxConstraints.expand(height: 64, width: 64), - style: ButtonStyle( - backgroundColor: - MaterialStateProperty.all(scale.secondaryScale.border), - shape: MaterialStateProperty.all(const RoundedRectangleBorder( - borderRadius: BorderRadius.all(Radius.circular(16))))), - tooltip: translate('app_bar.settings_tooltip'), - onPressed: () async { - context.go('/home/settings'); - }).paddingLTRB(0, 0, 8, 0), - ProfileWidget( - name: account.profile.name, - pronouns: account.profile.pronouns, - ).expanded(), - ]).paddingAll(8), - MainPager( - localAccounts: localAccounts, - activeUserLogin: activeUserLogin, - account: account) - .expanded() - ]); - } - - Widget buildUserPanel() { - final localAccountsV = ref.watch(localAccountsProvider); - final loginsV = ref.watch(loginsProvider); - - if (!localAccountsV.hasValue || !loginsV.hasValue) { - return waitingPage(context); - } - final localAccounts = localAccountsV.requireValue; - final logins = loginsV.requireValue; - - final activeUserLogin = logins.activeUserLogin; - if (activeUserLogin == null) { - // If no logged in user is active, show the list of account - return buildAccountList(); - } - final accountV = ref - .watch(fetchAccountProvider(accountMasterRecordKey: activeUserLogin)); - if (!accountV.hasValue) { - return waitingPage(context); - } - final account = accountV.requireValue; - switch (account.status) { - case AccountInfoStatus.noAccount: - Future.delayed(0.ms, () async { - await showErrorModal(context, translate('home.missing_account_title'), - translate('home.missing_account_text')); - // Delete account - await ref - .read(localAccountsProvider.notifier) - .deleteLocalAccount(activeUserLogin); - // Switch to no active user login - await ref.read(loginsProvider.notifier).switchToAccount(null); - }); - return waitingPage(context); - case AccountInfoStatus.accountInvalid: - Future.delayed(0.ms, () async { - await showErrorModal(context, translate('home.invalid_account_title'), - translate('home.invalid_account_text')); - // Delete account - await ref - .read(localAccountsProvider.notifier) - .deleteLocalAccount(activeUserLogin); - // Switch to no active user login - await ref.read(loginsProvider.notifier).switchToAccount(null); - }); - return waitingPage(context); - case AccountInfoStatus.accountLocked: - // Show unlock widget - return buildUnlockAccount(context, localAccounts); - case AccountInfoStatus.accountReady: - return buildReadyAccount( - context, - localAccounts, - activeUserLogin, - account.account!, - ); - } - } - - // ignore: prefer_expression_function_bodies - Widget buildPhone(BuildContext context) { - return Material(color: Colors.transparent, child: buildUserPanel()); - } - - // ignore: prefer_expression_function_bodies - Widget buildTabletLeftPane(BuildContext context) { - // - return Material(color: Colors.transparent, child: buildUserPanel()); - } - - // ignore: prefer_expression_function_bodies - Widget buildTabletRightPane(BuildContext context) { - // - return HomePage.buildChatComponent(context, ref); - } - - // ignore: prefer_expression_function_bodies - Widget buildTablet(BuildContext context) { - final w = MediaQuery.of(context).size.width; - final theme = Theme.of(context); - final scale = theme.extension()!; - - final children = [ - ConstrainedBox( - constraints: const BoxConstraints(minWidth: 300, maxWidth: 300), - child: ConstrainedBox( - constraints: BoxConstraints(maxWidth: w / 2), - child: buildTabletLeftPane(context))), - SizedBox( - width: 2, - height: double.infinity, - child: ColoredBox(color: scale.primaryScale.hoverBorder)), - Expanded(child: buildTabletRightPane(context)), - ]; - - return Row( - children: children, - ); - - // final theme = MultiSplitViewTheme( - // data: isDesktop - // ? MultiSplitViewThemeData( - // dividerThickness: 1, - // dividerPainter: DividerPainters.grooved2(thickness: 1)) - // : MultiSplitViewThemeData( - // dividerThickness: 3, - // dividerPainter: DividerPainters.grooved2(thickness: 1)), - // child: multiSplitView); - } - - @override - Widget build(BuildContext context) { - ref.watch(windowControlProvider); - - final theme = Theme.of(context); - final scale = theme.extension()!; - - return SafeArea( - child: GestureDetector( - onTap: () => FocusScope.of(context).requestFocus(_unfocusNode), - child: DecoratedBox( - decoration: BoxDecoration( - color: scale.primaryScale.activeElementBackground), - child: responsiveVisibility( - context: context, - phone: false, - ) - ? buildTablet(context) - : buildPhone(context), - ))); - } -} diff --git a/lib/old_to_refactor/providers/connection_state.dart b/lib/old_to_refactor/providers/connection_state.dart deleted file mode 100644 index a663190..0000000 --- a/lib/old_to_refactor/providers/connection_state.dart +++ /dev/null @@ -1,29 +0,0 @@ -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:freezed_annotation/freezed_annotation.dart'; - -import '../../../packages/veilid_support/veilid_support.dart'; - -part 'connection_state.freezed.dart'; - -@freezed -class ConnectionState with _$ConnectionState { - const factory ConnectionState({ - required VeilidStateAttachment attachment, - }) = _ConnectionState; - const ConnectionState._(); - - bool get isAttached => !(attachment.state == AttachmentState.detached || - attachment.state == AttachmentState.detaching || - attachment.state == AttachmentState.attaching); - - bool get isPublicInternetReady => attachment.publicInternetReady; -} - -final connectionState = StateController(const ConnectionState( - attachment: VeilidStateAttachment( - state: AttachmentState.detached, - publicInternetReady: false, - localNetworkReady: false))); -final connectionStateProvider = - StateNotifierProvider, ConnectionState>( - (ref) => connectionState); diff --git a/lib/old_to_refactor/providers/connection_state.freezed.dart b/lib/old_to_refactor/providers/connection_state.freezed.dart deleted file mode 100644 index 8ac0282..0000000 --- a/lib/old_to_refactor/providers/connection_state.freezed.dart +++ /dev/null @@ -1,138 +0,0 @@ -// coverage:ignore-file -// GENERATED CODE - DO NOT MODIFY BY HAND -// ignore_for_file: type=lint -// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark - -part of 'connection_state.dart'; - -// ************************************************************************** -// FreezedGenerator -// ************************************************************************** - -T _$identity(T value) => value; - -final _privateConstructorUsedError = UnsupportedError( - 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#custom-getters-and-methods'); - -/// @nodoc -mixin _$ConnectionState { - VeilidStateAttachment get attachment => throw _privateConstructorUsedError; - - @JsonKey(ignore: true) - $ConnectionStateCopyWith get copyWith => - throw _privateConstructorUsedError; -} - -/// @nodoc -abstract class $ConnectionStateCopyWith<$Res> { - factory $ConnectionStateCopyWith( - ConnectionState value, $Res Function(ConnectionState) then) = - _$ConnectionStateCopyWithImpl<$Res, ConnectionState>; - @useResult - $Res call({VeilidStateAttachment attachment}); -} - -/// @nodoc -class _$ConnectionStateCopyWithImpl<$Res, $Val extends ConnectionState> - implements $ConnectionStateCopyWith<$Res> { - _$ConnectionStateCopyWithImpl(this._value, this._then); - - // ignore: unused_field - final $Val _value; - // ignore: unused_field - final $Res Function($Val) _then; - - @pragma('vm:prefer-inline') - @override - $Res call({ - Object? attachment = freezed, - }) { - return _then(_value.copyWith( - attachment: freezed == attachment - ? _value.attachment - : attachment // ignore: cast_nullable_to_non_nullable - as VeilidStateAttachment, - ) as $Val); - } -} - -/// @nodoc -abstract class _$$ConnectionStateImplCopyWith<$Res> - implements $ConnectionStateCopyWith<$Res> { - factory _$$ConnectionStateImplCopyWith(_$ConnectionStateImpl value, - $Res Function(_$ConnectionStateImpl) then) = - __$$ConnectionStateImplCopyWithImpl<$Res>; - @override - @useResult - $Res call({VeilidStateAttachment attachment}); -} - -/// @nodoc -class __$$ConnectionStateImplCopyWithImpl<$Res> - extends _$ConnectionStateCopyWithImpl<$Res, _$ConnectionStateImpl> - implements _$$ConnectionStateImplCopyWith<$Res> { - __$$ConnectionStateImplCopyWithImpl( - _$ConnectionStateImpl _value, $Res Function(_$ConnectionStateImpl) _then) - : super(_value, _then); - - @pragma('vm:prefer-inline') - @override - $Res call({ - Object? attachment = freezed, - }) { - return _then(_$ConnectionStateImpl( - attachment: freezed == attachment - ? _value.attachment - : attachment // ignore: cast_nullable_to_non_nullable - as VeilidStateAttachment, - )); - } -} - -/// @nodoc - -class _$ConnectionStateImpl extends _ConnectionState { - const _$ConnectionStateImpl({required this.attachment}) : super._(); - - @override - final VeilidStateAttachment attachment; - - @override - String toString() { - return 'ConnectionState(attachment: $attachment)'; - } - - @override - bool operator ==(Object other) { - return identical(this, other) || - (other.runtimeType == runtimeType && - other is _$ConnectionStateImpl && - const DeepCollectionEquality() - .equals(other.attachment, attachment)); - } - - @override - int get hashCode => - Object.hash(runtimeType, const DeepCollectionEquality().hash(attachment)); - - @JsonKey(ignore: true) - @override - @pragma('vm:prefer-inline') - _$$ConnectionStateImplCopyWith<_$ConnectionStateImpl> get copyWith => - __$$ConnectionStateImplCopyWithImpl<_$ConnectionStateImpl>( - this, _$identity); -} - -abstract class _ConnectionState extends ConnectionState { - const factory _ConnectionState( - {required final VeilidStateAttachment attachment}) = - _$ConnectionStateImpl; - const _ConnectionState._() : super._(); - - @override - VeilidStateAttachment get attachment; - @override - @JsonKey(ignore: true) - _$$ConnectionStateImplCopyWith<_$ConnectionStateImpl> get copyWith => - throw _privateConstructorUsedError; -} diff --git a/lib/processor.dart b/lib/processor.dart deleted file mode 100644 index d0c359a..0000000 --- a/lib/processor.dart +++ /dev/null @@ -1,103 +0,0 @@ -import 'dart:async'; - -import 'package:veilid/veilid.dart'; - -import 'app.dart'; -import 'old_to_refactor/providers/connection_state.dart'; -import 'tools/tools.dart'; -import '../packages/veilid_support/src/config.dart'; -import '../packages/veilid_support/src/veilid_log.dart'; - -class Processor { - Processor(); - String _veilidVersion = ''; - bool _startedUp = false; - Stream? _updateStream; - Future? _updateProcessor; - - Future startup() async { - if (_startedUp) { - return; - } - - try { - _veilidVersion = Veilid.instance.veilidVersionString(); - } on Exception { - _veilidVersion = 'Failed to get veilid version.'; - } - - log.info('Veilid version: $_veilidVersion'); - - // HACK: In case of hot restart shut down first - try { - await Veilid.instance.shutdownVeilidCore(); - } on Exception { - // Do nothing on failure here - } - - final updateStream = await Veilid.instance - .startupVeilidCore(await getVeilidConfig(VeilidChatApp.name)); - _updateStream = updateStream; - _updateProcessor = processUpdates(); - _startedUp = true; - - await Veilid.instance.attach(); - } - - Future shutdown() async { - if (!_startedUp) { - return; - } - await Veilid.instance.shutdownVeilidCore(); - if (_updateProcessor != null) { - await _updateProcessor; - } - _updateProcessor = null; - _updateStream = null; - _startedUp = false; - } - - Future processUpdateAttachment( - VeilidUpdateAttachment updateAttachment) async { - //loggy.info("Attachment: ${updateAttachment.json}"); - - // // Set connection meter and ui state for connection state - - connectionState.state = ConnectionState( - attachment: VeilidStateAttachment( - state: updateAttachment.state, - publicInternetReady: updateAttachment.publicInternetReady, - localNetworkReady: updateAttachment.localNetworkReady)); - } - - Future processUpdateConfig(VeilidUpdateConfig updateConfig) async { - //loggy.info("Config: ${updateConfig.json}"); - } - - Future processUpdateNetwork(VeilidUpdateNetwork updateNetwork) async { - //loggy.info("Network: ${updateNetwork.json}"); - } - - Future processUpdates() async { - final stream = _updateStream; - if (stream != null) { - await for (final update in stream) { - if (update is VeilidLog) { - await processLog(update); - } else if (update is VeilidUpdateAttachment) { - await processUpdateAttachment(update); - } else if (update is VeilidUpdateConfig) { - await processUpdateConfig(update); - } else if (update is VeilidUpdateNetwork) { - await processUpdateNetwork(update); - } else if (update is VeilidAppMessage) { - log.info('AppMessage: ${update.toJson()}'); - } else if (update is VeilidAppCall) { - log.info('AppCall: ${update.toJson()}'); - } else { - log.trace('Update: ${update.toJson()}'); - } - } - } - } -} diff --git a/lib/router/cubit/router_cubit.dart b/lib/router/cubit/router_cubit.dart index 5fcd379..b182a27 100644 --- a/lib/router/cubit/router_cubit.dart +++ b/lib/router/cubit/router_cubit.dart @@ -5,15 +5,14 @@ import 'package:flutter/widgets.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:go_router/go_router.dart'; +import '../../../account_manager/account_manager.dart'; import '../../init.dart'; -import '../../local_account_manager/respository/account_repository/account_repository.dart'; import '../../old_to_refactor/pages/chat_only.dart'; -import '../../old_to_refactor/pages/developer.dart'; import '../../old_to_refactor/pages/home.dart'; import '../../old_to_refactor/pages/index.dart'; -import '../../account_manager/view/new_account_page/new_account_page.dart'; import '../../old_to_refactor/pages/settings.dart'; import '../../tools/tools.dart'; +import '../../veilid_processor/views/developer.dart'; part 'router_cubit.freezed.dart'; part 'router_cubit.g.dart'; diff --git a/lib/theme/models/models.dart b/lib/theme/models/models.dart new file mode 100644 index 0000000..e5e69d9 --- /dev/null +++ b/lib/theme/models/models.dart @@ -0,0 +1,4 @@ +export 'scale_color.dart'; +export 'scale_scheme.dart'; +export 'theme_preference.dart'; +export 'radix_generator.dart'; diff --git a/lib/theme/radix_generator.dart b/lib/theme/models/radix_generator.dart similarity index 100% rename from lib/theme/radix_generator.dart rename to lib/theme/models/radix_generator.dart diff --git a/lib/theme/scale_color.dart b/lib/theme/models/scale_color.dart similarity index 100% rename from lib/theme/scale_color.dart rename to lib/theme/models/scale_color.dart diff --git a/lib/theme/scale_scheme.dart b/lib/theme/models/scale_scheme.dart similarity index 100% rename from lib/theme/scale_scheme.dart rename to lib/theme/models/scale_scheme.dart diff --git a/lib/theme/theme_preference.dart b/lib/theme/models/theme_preference.dart similarity index 100% rename from lib/theme/theme_preference.dart rename to lib/theme/models/theme_preference.dart diff --git a/lib/theme/theme_preference.freezed.dart b/lib/theme/models/theme_preference.freezed.dart similarity index 100% rename from lib/theme/theme_preference.freezed.dart rename to lib/theme/models/theme_preference.freezed.dart diff --git a/lib/theme/theme_preference.g.dart b/lib/theme/models/theme_preference.g.dart similarity index 100% rename from lib/theme/theme_preference.g.dart rename to lib/theme/models/theme_preference.g.dart diff --git a/lib/theme/theme_repository.dart b/lib/theme/repository/theme_repository.dart similarity index 98% rename from lib/theme/theme_repository.dart rename to lib/theme/repository/theme_repository.dart index c717928..2aae8cd 100644 --- a/lib/theme/theme_repository.dart +++ b/lib/theme/repository/theme_repository.dart @@ -5,8 +5,7 @@ import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:shared_preferences/shared_preferences.dart'; -import 'radix_generator.dart'; -import 'theme_preference.dart'; +import '../models/models.dart'; //////////////////////////////////////////////////////////////////////// diff --git a/lib/theme/theme.dart b/lib/theme/theme.dart index 064b1bf..21be85e 100644 --- a/lib/theme/theme.dart +++ b/lib/theme/theme.dart @@ -1,3 +1,2 @@ -export 'scale_scheme.dart'; -export 'theme_preference.dart'; -export 'theme_repository.dart'; +export 'models/models.dart'; +export 'repository/theme_repository.dart'; diff --git a/lib/tick.dart b/lib/tick.dart index 82909bc..bb495a2 100644 --- a/lib/tick.dart +++ b/lib/tick.dart @@ -1,18 +1,11 @@ -// XXX Eliminate this when we have ValueChanged import 'dart:async'; -import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:veilid_support/veilid_support.dart'; import 'init.dart'; -import 'old_to_refactor/providers/account.dart'; -import 'old_to_refactor/providers/chat.dart'; -import 'old_to_refactor/providers/connection_state.dart'; -import 'old_to_refactor/providers/contact.dart'; -import 'old_to_refactor/providers/contact_invite.dart'; -import 'old_to_refactor/providers/conversation.dart'; -import 'proto/proto.dart' as proto; +import 'veilid_processor/veilid_processor.dart'; const int ticksPerContactInvitationCheck = 5; const int ticksPerNewMessageCheck = 5; @@ -35,6 +28,9 @@ class BackgroundTicker extends StatefulWidget { class BackgroundTickerState extends State { Timer? _tickTimer; bool _inTick = false; + bool _inDoContactInvitationCheck = false; + bool _inDoNewMessageCheck = false; + int _contactInvitationCheckTick = 0; int _newMessageCheckTick = 0; @@ -65,32 +61,38 @@ class BackgroundTickerState extends State { } Future _onTick() async { - // Don't tick until veilid is started and attached - if (!eventualVeilid.isCompleted) { + // Don't tick until we are initialized + if (!eventualInitialized.isCompleted) { return; } - if (!connectionState.state.isAttached) { + if (!ProcessorRepository + .instance.processorConnectionState.isPublicInternetReady) { return; } _inTick = true; try { - final unord = >[]; + // Tick DHT record pool + if (!DHTRecordPool.instance.inTick) { + unawaited(DHTRecordPool.instance.tick()); + } + // Check extant contact invitations once every N seconds _contactInvitationCheckTick += 1; if (_contactInvitationCheckTick >= ticksPerContactInvitationCheck) { _contactInvitationCheckTick = 0; - unord.add(_doContactInvitationCheck()); + if (!_inDoContactInvitationCheck) { + unawaited(_doContactInvitationCheck()); + } } // Check new messages once every N seconds _newMessageCheckTick += 1; if (_newMessageCheckTick >= ticksPerNewMessageCheck) { _newMessageCheckTick = 0; - unord.add(_doNewMessageCheck()); - } - if (unord.isNotEmpty) { - await Future.wait(unord); + if (!_inDoNewMessageCheck) { + unawaited(_doNewMessageCheck()); + } } } finally { _inTick = false; @@ -98,96 +100,118 @@ class BackgroundTickerState extends State { } Future _doContactInvitationCheck() async { - if (!connectionState.state.isPublicInternetReady) { - return; - } - final contactInvitationRecords = - await ref.read(fetchContactInvitationRecordsProvider.future); - if (contactInvitationRecords == null) { - return; - } - final activeAccountInfo = await ref.read(fetchActiveAccountProvider.future); - if (activeAccountInfo == null) { + if (_inDoContactInvitationCheck) { return; } + _inDoContactInvitationCheck = true; - final allChecks = >[]; - for (final contactInvitationRecord in contactInvitationRecords) { - allChecks.add(() async { - final acceptReject = await checkAcceptRejectContact( - activeAccountInfo: activeAccountInfo, - contactInvitationRecord: contactInvitationRecord); - if (acceptReject != null) { - final acceptedContact = acceptReject.acceptedContact; - if (acceptedContact != null) { - // Accept - await createContact( - activeAccountInfo: activeAccountInfo, - profile: acceptedContact.profile, - remoteIdentity: acceptedContact.remoteIdentity, - remoteConversationRecordKey: - acceptedContact.remoteConversationRecordKey, - localConversationRecordKey: - acceptedContact.localConversationRecordKey, - ); - ref - ..invalidate(fetchContactInvitationRecordsProvider) - ..invalidate(fetchContactListProvider); - } else { - // Reject - ref.invalidate(fetchContactInvitationRecordsProvider); - } - } - }()); + if (!ProcessorRepository + .instance.processorConnectionState.isPublicInternetReady) { + return; + } + // final contactInvitationRecords = + // await ref.read(fetchContactInvitationRecordsProvider.future); + // if (contactInvitationRecords == null) { + // return; + // } + try { + // final activeAccountInfo = + // await ref.read(fetchActiveAccountProvider.future); + // if (activeAccountInfo == null) { + // return; + // } + + // final allChecks = >[]; + // for (final contactInvitationRecord in contactInvitationRecords) { + // allChecks.add(() async { + // final acceptReject = await checkAcceptRejectContact( + // activeAccountInfo: activeAccountInfo, + // contactInvitationRecord: contactInvitationRecord); + // if (acceptReject != null) { + // final acceptedContact = acceptReject.acceptedContact; + // if (acceptedContact != null) { + // // Accept + // await createContact( + // activeAccountInfo: activeAccountInfo, + // profile: acceptedContact.profile, + // remoteIdentity: acceptedContact.remoteIdentity, + // remoteConversationRecordKey: + // acceptedContact.remoteConversationRecordKey, + // localConversationRecordKey: + // acceptedContact.localConversationRecordKey, + // ); + // ref + // ..invalidate(fetchContactInvitationRecordsProvider) + // ..invalidate(fetchContactListProvider); + // } else { + // // Reject + // ref.invalidate(fetchContactInvitationRecordsProvider); + // } + // } + // }()); + // } + // await Future.wait(allChecks); + } finally { + _inDoContactInvitationCheck = true; } - await Future.wait(allChecks); } Future _doNewMessageCheck() async { - if (!connectionState.state.isPublicInternetReady) { - return; - } - final activeChat = ref.read(activeChatStateProvider); - if (activeChat == null) { - return; - } - final activeAccountInfo = await ref.read(fetchActiveAccountProvider.future); - if (activeAccountInfo == null) { + if (_inDoNewMessageCheck) { return; } + _inDoNewMessageCheck = true; - final contactList = ref.read(fetchContactListProvider).asData?.value ?? - const IListConst([]); - - final activeChatContactIdx = contactList.indexWhere( - (c) => - proto.TypedKeyProto.fromProto(c.remoteConversationRecordKey) == - activeChat, - ); - if (activeChatContactIdx == -1) { - return; - } - final activeChatContact = contactList[activeChatContactIdx]; - final remoteIdentityPublicKey = - proto.TypedKeyProto.fromProto(activeChatContact.identityPublicKey); - final remoteConversationRecordKey = proto.TypedKeyProto.fromProto( - activeChatContact.remoteConversationRecordKey); - final localConversationRecordKey = proto.TypedKeyProto.fromProto( - activeChatContact.localConversationRecordKey); - - final newMessages = await getRemoteConversationMessages( - activeAccountInfo: activeAccountInfo, - remoteIdentityPublicKey: remoteIdentityPublicKey, - remoteConversationRecordKey: remoteConversationRecordKey); - if (newMessages != null && newMessages.isNotEmpty) { - final changed = await mergeLocalConversationMessages( - activeAccountInfo: activeAccountInfo, - localConversationRecordKey: localConversationRecordKey, - remoteIdentityPublicKey: remoteIdentityPublicKey, - newMessages: newMessages); - if (changed) { - ref.invalidate(activeConversationMessagesProvider); + try { + if (!ProcessorRepository + .instance.processorConnectionState.isPublicInternetReady) { + return; } + // final activeChat = ref.read(activeChatStateProvider); + // if (activeChat == null) { + // return; + // } + // final activeAccountInfo = + // await ref.read(fetchActiveAccountProvider.future); + // if (activeAccountInfo == null) { + // return; + // } + + // final contactList = ref.read(fetchContactListProvider).asData?.value ?? + // const IListConst([]); + + // final activeChatContactIdx = contactList.indexWhere( + // (c) => + // proto.TypedKeyProto.fromProto(c.remoteConversationRecordKey) == + // activeChat, + // ); + // if (activeChatContactIdx == -1) { + // return; + // } + // final activeChatContact = contactList[activeChatContactIdx]; + // final remoteIdentityPublicKey = + // proto.TypedKeyProto.fromProto(activeChatContact.identityPublicKey); + // final remoteConversationRecordKey = proto.TypedKeyProto.fromProto( + // activeChatContact.remoteConversationRecordKey); + // final localConversationRecordKey = proto.TypedKeyProto.fromProto( + // activeChatContact.localConversationRecordKey); + + // final newMessages = await getRemoteConversationMessages( + // activeAccountInfo: activeAccountInfo, + // remoteIdentityPublicKey: remoteIdentityPublicKey, + // remoteConversationRecordKey: remoteConversationRecordKey); + // if (newMessages != null && newMessages.isNotEmpty) { + // final changed = await mergeLocalConversationMessages( + // activeAccountInfo: activeAccountInfo, + // localConversationRecordKey: localConversationRecordKey, + // remoteIdentityPublicKey: remoteIdentityPublicKey, + // newMessages: newMessages); + // if (changed) { + // ref.invalidate(activeConversationMessagesProvider); + // } + // } + } finally { + _inDoNewMessageCheck = false; } } } diff --git a/lib/tools/loggy.dart b/lib/tools/loggy.dart index 3be0f5c..9947308 100644 --- a/lib/tools/loggy.dart +++ b/lib/tools/loggy.dart @@ -6,9 +6,9 @@ import 'package:flutter/foundation.dart'; import 'package:flutter_translate/flutter_translate.dart'; import 'package:intl/intl.dart'; import 'package:loggy/loggy.dart'; +import 'package:veilid_support/veilid_support.dart'; -import '../old_to_refactor/pages/developer.dart'; -import '../../packages/veilid_support/veilid_support.dart'; +import '../veilid_processor/views/developer.dart'; import 'state_logger.dart'; String wrapWithLogColor(LogLevel? level, String text) { diff --git a/lib/tools/stream_wrapper_cubit.dart b/lib/tools/stream_wrapper_cubit.dart new file mode 100644 index 0000000..a858cb1 --- /dev/null +++ b/lib/tools/stream_wrapper_cubit.dart @@ -0,0 +1,25 @@ +import 'dart:async'; + +import 'package:bloc/bloc.dart'; +import 'package:veilid_support/veilid_support.dart'; + +abstract class StreamWrapperCubit extends Cubit> { + StreamWrapperCubit(Stream stream, {State? defaultState}) + : super(defaultState != null + ? AsyncValue.data(defaultState) + : const AsyncValue.loading()) { + _subscription = stream.listen((event) => emit(AsyncValue.data(event)), + // ignore: avoid_types_on_closure_parameters + onError: (Object error, StackTrace stackTrace) { + emit(AsyncValue.error(error, stackTrace)); + }); + + @override + Future close() async { + await _subscription.cancel(); + await super.close(); + } + } + + late final StreamSubscription _subscription; +} diff --git a/lib/tools/tools.dart b/lib/tools/tools.dart index 203cfc3..147829f 100644 --- a/lib/tools/tools.dart +++ b/lib/tools/tools.dart @@ -1,10 +1,9 @@ export 'animations.dart'; -export 'async_table_db_backed_cubit.dart'; -export 'async_value.dart'; export 'loggy.dart'; export 'phono_byte.dart'; export 'responsive.dart'; export 'scanner_error_widget.dart'; export 'state_logger.dart'; +export 'stream_wrapper_cubit.dart'; export 'widget_helpers.dart'; export 'window_control.dart'; diff --git a/lib/veilid_processor/cubit/connection_state_cubit.dart b/lib/veilid_processor/cubit/connection_state_cubit.dart new file mode 100644 index 0000000..e3ef7fa --- /dev/null +++ b/lib/veilid_processor/cubit/connection_state_cubit.dart @@ -0,0 +1,12 @@ +import '../../tools/tools.dart'; +import '../models/models.dart'; +import '../repository/processor_repository.dart'; + +export '../models/processor_connection_state.dart'; + +class ConnectionStateCubit + extends StreamWrapperCubit { + ConnectionStateCubit(ProcessorRepository processorRepository) + : super(processorRepository.streamProcessorConnectionState(), + defaultState: processorRepository.processorConnectionState); +} diff --git a/lib/veilid_processor/models/models.dart b/lib/veilid_processor/models/models.dart new file mode 100644 index 0000000..4dd8061 --- /dev/null +++ b/lib/veilid_processor/models/models.dart @@ -0,0 +1 @@ +export 'processor_connection_state.dart'; diff --git a/lib/veilid_processor/models/processor_connection_state.dart b/lib/veilid_processor/models/processor_connection_state.dart new file mode 100644 index 0000000..c5220fb --- /dev/null +++ b/lib/veilid_processor/models/processor_connection_state.dart @@ -0,0 +1,19 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:veilid_support/veilid_support.dart'; + +part 'processor_connection_state.freezed.dart'; + +@freezed +class ProcessorConnectionState with _$ProcessorConnectionState { + const factory ProcessorConnectionState({ + required VeilidStateAttachment attachment, + required VeilidStateNetwork network, + }) = _ProcessorConnectionState; + const ProcessorConnectionState._(); + + bool get isAttached => !(attachment.state == AttachmentState.detached || + attachment.state == AttachmentState.detaching || + attachment.state == AttachmentState.attaching); + + bool get isPublicInternetReady => attachment.publicInternetReady; +} diff --git a/lib/veilid_processor/models/processor_connection_state.freezed.dart b/lib/veilid_processor/models/processor_connection_state.freezed.dart new file mode 100644 index 0000000..a6e01fa --- /dev/null +++ b/lib/veilid_processor/models/processor_connection_state.freezed.dart @@ -0,0 +1,184 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'processor_connection_state.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +final _privateConstructorUsedError = UnsupportedError( + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#custom-getters-and-methods'); + +/// @nodoc +mixin _$ProcessorConnectionState { + VeilidStateAttachment get attachment => throw _privateConstructorUsedError; + VeilidStateNetwork get network => throw _privateConstructorUsedError; + + @JsonKey(ignore: true) + $ProcessorConnectionStateCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $ProcessorConnectionStateCopyWith<$Res> { + factory $ProcessorConnectionStateCopyWith(ProcessorConnectionState value, + $Res Function(ProcessorConnectionState) then) = + _$ProcessorConnectionStateCopyWithImpl<$Res, ProcessorConnectionState>; + @useResult + $Res call({VeilidStateAttachment attachment, VeilidStateNetwork network}); + + $VeilidStateAttachmentCopyWith<$Res> get attachment; + $VeilidStateNetworkCopyWith<$Res> get network; +} + +/// @nodoc +class _$ProcessorConnectionStateCopyWithImpl<$Res, + $Val extends ProcessorConnectionState> + implements $ProcessorConnectionStateCopyWith<$Res> { + _$ProcessorConnectionStateCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? attachment = null, + Object? network = null, + }) { + return _then(_value.copyWith( + attachment: null == attachment + ? _value.attachment + : attachment // ignore: cast_nullable_to_non_nullable + as VeilidStateAttachment, + network: null == network + ? _value.network + : network // ignore: cast_nullable_to_non_nullable + as VeilidStateNetwork, + ) as $Val); + } + + @override + @pragma('vm:prefer-inline') + $VeilidStateAttachmentCopyWith<$Res> get attachment { + return $VeilidStateAttachmentCopyWith<$Res>(_value.attachment, (value) { + return _then(_value.copyWith(attachment: value) as $Val); + }); + } + + @override + @pragma('vm:prefer-inline') + $VeilidStateNetworkCopyWith<$Res> get network { + return $VeilidStateNetworkCopyWith<$Res>(_value.network, (value) { + return _then(_value.copyWith(network: value) as $Val); + }); + } +} + +/// @nodoc +abstract class _$$ProcessorConnectionStateImplCopyWith<$Res> + implements $ProcessorConnectionStateCopyWith<$Res> { + factory _$$ProcessorConnectionStateImplCopyWith( + _$ProcessorConnectionStateImpl value, + $Res Function(_$ProcessorConnectionStateImpl) then) = + __$$ProcessorConnectionStateImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({VeilidStateAttachment attachment, VeilidStateNetwork network}); + + @override + $VeilidStateAttachmentCopyWith<$Res> get attachment; + @override + $VeilidStateNetworkCopyWith<$Res> get network; +} + +/// @nodoc +class __$$ProcessorConnectionStateImplCopyWithImpl<$Res> + extends _$ProcessorConnectionStateCopyWithImpl<$Res, + _$ProcessorConnectionStateImpl> + implements _$$ProcessorConnectionStateImplCopyWith<$Res> { + __$$ProcessorConnectionStateImplCopyWithImpl( + _$ProcessorConnectionStateImpl _value, + $Res Function(_$ProcessorConnectionStateImpl) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? attachment = null, + Object? network = null, + }) { + return _then(_$ProcessorConnectionStateImpl( + attachment: null == attachment + ? _value.attachment + : attachment // ignore: cast_nullable_to_non_nullable + as VeilidStateAttachment, + network: null == network + ? _value.network + : network // ignore: cast_nullable_to_non_nullable + as VeilidStateNetwork, + )); + } +} + +/// @nodoc + +class _$ProcessorConnectionStateImpl extends _ProcessorConnectionState { + const _$ProcessorConnectionStateImpl( + {required this.attachment, required this.network}) + : super._(); + + @override + final VeilidStateAttachment attachment; + @override + final VeilidStateNetwork network; + + @override + String toString() { + return 'ProcessorConnectionState(attachment: $attachment, network: $network)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$ProcessorConnectionStateImpl && + (identical(other.attachment, attachment) || + other.attachment == attachment) && + (identical(other.network, network) || other.network == network)); + } + + @override + int get hashCode => Object.hash(runtimeType, attachment, network); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$ProcessorConnectionStateImplCopyWith<_$ProcessorConnectionStateImpl> + get copyWith => __$$ProcessorConnectionStateImplCopyWithImpl< + _$ProcessorConnectionStateImpl>(this, _$identity); +} + +abstract class _ProcessorConnectionState extends ProcessorConnectionState { + const factory _ProcessorConnectionState( + {required final VeilidStateAttachment attachment, + required final VeilidStateNetwork network}) = + _$ProcessorConnectionStateImpl; + const _ProcessorConnectionState._() : super._(); + + @override + VeilidStateAttachment get attachment; + @override + VeilidStateNetwork get network; + @override + @JsonKey(ignore: true) + _$$ProcessorConnectionStateImplCopyWith<_$ProcessorConnectionStateImpl> + get copyWith => throw _privateConstructorUsedError; +} diff --git a/lib/veilid_processor/repository/processor_repository.dart b/lib/veilid_processor/repository/processor_repository.dart new file mode 100644 index 0000000..fa99a95 --- /dev/null +++ b/lib/veilid_processor/repository/processor_repository.dart @@ -0,0 +1,132 @@ +import 'dart:async'; + +import 'package:flutter/foundation.dart'; +import 'package:veilid_support/veilid_support.dart'; + +import '../../app.dart'; +import '../../tools/tools.dart'; +import '../models/models.dart'; + +class ProcessorRepository { + ProcessorRepository._() + : startedUp = false, + _controllerConnectionState = StreamController.broadcast(sync: true), + processorConnectionState = ProcessorConnectionState( + attachment: const VeilidStateAttachment( + state: AttachmentState.detached, + publicInternetReady: false, + localNetworkReady: false), + network: VeilidStateNetwork( + started: false, + bpsDown: BigInt.zero, + bpsUp: BigInt.zero, + peers: [])); + + ////////////////////////////////////////////////////////////// + /// Singleton initialization + + static ProcessorRepository instance = ProcessorRepository._(); + + Future startup() async { + if (startedUp) { + return; + } + + var veilidVersion = ''; + + try { + veilidVersion = Veilid.instance.veilidVersionString(); + } on Exception { + veilidVersion = 'Failed to get veilid version.'; + } + + log.info('Veilid version: $veilidVersion'); + + // HACK: In case of hot restart shut down first + try { + await Veilid.instance.shutdownVeilidCore(); + } on Exception { + // Do nothing on failure here + } + + final updateStream = await Veilid.instance + .startupVeilidCore(await getVeilidConfig(kIsWeb, VeilidChatApp.name)); + _updateSubscription = updateStream.listen((update) { + if (update is VeilidLog) { + processLog(update); + } else if (update is VeilidUpdateAttachment) { + processUpdateAttachment(update); + } else if (update is VeilidUpdateConfig) { + processUpdateConfig(update); + } else if (update is VeilidUpdateNetwork) { + processUpdateNetwork(update); + } else if (update is VeilidAppMessage) { + processAppMessage(update); + } else if (update is VeilidAppCall) { + log.info('AppCall: ${update.toJson()}'); + } else if (update is VeilidUpdateValueChange) { + processUpdateValueChange(update); + } else { + log.trace('Update: ${update.toJson()}'); + } + }); + + startedUp = true; + + await Veilid.instance.attach(); + } + + Future shutdown() async { + if (!startedUp) { + return; + } + await Veilid.instance.shutdownVeilidCore(); + await _updateSubscription?.cancel(); + _updateSubscription = null; + + startedUp = false; + } + + Stream streamProcessorConnectionState() => + _controllerConnectionState.stream; + + void processUpdateAttachment(VeilidUpdateAttachment updateAttachment) { + // Set connection meter and ui state for connection state + processorConnectionState = processorConnectionState.copyWith( + attachment: VeilidStateAttachment( + state: updateAttachment.state, + publicInternetReady: updateAttachment.publicInternetReady, + localNetworkReady: updateAttachment.localNetworkReady)); + } + + void processUpdateConfig(VeilidUpdateConfig updateConfig) { + log.debug('VeilidUpdateConfig: ${updateConfig.toJson()}'); + } + + void processUpdateNetwork(VeilidUpdateNetwork updateNetwork) { + // Set connection meter and ui state for connection state + processorConnectionState = processorConnectionState.copyWith( + network: VeilidStateNetwork( + started: updateNetwork.started, + bpsDown: updateNetwork.bpsDown, + bpsUp: updateNetwork.bpsUp, + peers: updateNetwork.peers)); + _controllerConnectionState.add(processorConnectionState); + } + + void processAppMessage(VeilidAppMessage appMessage) { + log.debug('VeilidAppMessage: ${appMessage.toJson()}'); + } + + void processUpdateValueChange(VeilidUpdateValueChange updateValueChange) { + // Send value updates to DHTRecordPool + DHTRecordPool.instance.processUpdateValueChange(updateValueChange); + } + + //////////////////////////////////////////// + + StreamSubscription? _updateSubscription; + final StreamController _controllerConnectionState; + bool startedUp; + ProcessorConnectionState processorConnectionState; +} diff --git a/lib/veilid_processor/veilid_processor.dart b/lib/veilid_processor/veilid_processor.dart new file mode 100644 index 0000000..12d36bd --- /dev/null +++ b/lib/veilid_processor/veilid_processor.dart @@ -0,0 +1,4 @@ +export 'cubit/connection_state_cubit.dart'; +export 'models/models.dart'; +export 'repository/processor_repository.dart'; +export 'views/views.dart'; diff --git a/lib/old_to_refactor/pages/developer.dart b/lib/veilid_processor/views/developer.dart similarity index 98% rename from lib/old_to_refactor/pages/developer.dart rename to lib/veilid_processor/views/developer.dart index 1dc56cd..f738d6c 100644 --- a/lib/old_to_refactor/pages/developer.dart +++ b/lib/veilid_processor/views/developer.dart @@ -6,15 +6,15 @@ 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'; import 'package:go_router/go_router.dart'; import 'package:loggy/loggy.dart'; import 'package:quickalert/quickalert.dart'; +import 'package:veilid_support/veilid_support.dart'; import 'package:xterm/xterm.dart'; +import '../../theme/theme.dart'; import '../../tools/tools.dart'; -import '../../../packages/veilid_support/veilid_support.dart'; final globalDebugTerminal = Terminal( maxLines: 50000, @@ -32,7 +32,7 @@ class DeveloperPage extends StatefulWidget { DeveloperPageState createState() => DeveloperPageState(); } -class DeveloperPageState extends ConsumerState { +class DeveloperPageState extends State { final _terminalController = TerminalController(); final _debugCommandController = TextEditingController(); final _logLevelController = DropdownController(duration: 250.ms); diff --git a/lib/veilid_processor/views/signal_strength_meter.dart b/lib/veilid_processor/views/signal_strength_meter.dart new file mode 100644 index 0000000..9b189f4 --- /dev/null +++ b/lib/veilid_processor/views/signal_strength_meter.dart @@ -0,0 +1,88 @@ +import 'package:awesome_extensions/awesome_extensions.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:go_router/go_router.dart'; +import 'package:quickalert/quickalert.dart'; +import 'package:signal_strength_indicator/signal_strength_indicator.dart'; +import 'package:veilid_support/veilid_support.dart'; + +import '../../theme/theme.dart'; +import '../cubit/connection_state_cubit.dart'; + +class SignalStrengthMeterWidget extends StatelessWidget { + const SignalStrengthMeterWidget({super.key}); + + @override + // ignore: prefer_expression_function_bodies + Widget build(BuildContext context) { + final theme = Theme.of(context); + final scale = theme.extension()!; + + const iconSize = 16.0; + + return BlocBuilder>(builder: (context, state) { + late final Widget iconWidget; + state.when( + data: (connectionState) { + late final double value; + late final Color color; + late final Color inactiveColor; + + switch (connectionState.attachment.state) { + case AttachmentState.detached: + iconWidget = Icon(Icons.signal_cellular_nodata, + size: iconSize, color: scale.grayScale.text); + return; + case AttachmentState.detaching: + iconWidget = Icon(Icons.signal_cellular_off, + size: iconSize, color: scale.grayScale.text); + return; + case AttachmentState.attaching: + value = 0; + color = scale.primaryScale.text; + case AttachmentState.attachedWeak: + value = 1; + color = scale.primaryScale.text; + case AttachmentState.attachedStrong: + value = 2; + color = scale.primaryScale.text; + case AttachmentState.attachedGood: + value = 3; + color = scale.primaryScale.text; + case AttachmentState.fullyAttached: + value = 4; + color = scale.primaryScale.text; + case AttachmentState.overAttached: + value = 4; + color = scale.secondaryScale.subtleText; + } + inactiveColor = scale.grayScale.subtleText; + + iconWidget = SignalStrengthIndicator.bars( + value: value, + activeColor: color, + inactiveColor: inactiveColor, + size: iconSize, + barCount: 4, + spacing: 1); + }, + loading: () => {iconWidget = const Icon(Icons.warning)}, + error: (e, st) => { + iconWidget = const Icon(Icons.error).onTap( + () async => QuickAlert.show( + type: QuickAlertType.error, + context: context, + title: 'Error', + text: 'Error: {e}\n StackTrace: {st}'), + ) + }); + + return GestureDetector( + onLongPress: () async { + await GoRouterHelper(context).push('/developer'); + }, + child: iconWidget); + }); + } +} diff --git a/lib/veilid_processor/views/views.dart b/lib/veilid_processor/views/views.dart new file mode 100644 index 0000000..3d70862 --- /dev/null +++ b/lib/veilid_processor/views/views.dart @@ -0,0 +1,2 @@ +export 'developer.dart'; +export 'signal_strength_meter.dart'; diff --git a/packages/veilid_support/lib/dht_support/dht_support.dart b/packages/veilid_support/lib/dht_support/dht_support.dart index d4f0b09..0d56e45 100644 --- a/packages/veilid_support/lib/dht_support/dht_support.dart +++ b/packages/veilid_support/lib/dht_support/dht_support.dart @@ -4,5 +4,6 @@ library dht_support; export 'src/dht_record.dart'; export 'src/dht_record_crypto.dart'; +export 'src/dht_record_cubit.dart'; export 'src/dht_record_pool.dart'; export 'src/dht_short_array.dart'; diff --git a/packages/veilid_support/lib/dht_support/src/dht_record.dart b/packages/veilid_support/lib/dht_support/src/dht_record.dart index bc6dea2..f810974 100644 --- a/packages/veilid_support/lib/dht_support/src/dht_record.dart +++ b/packages/veilid_support/lib/dht_support/src/dht_record.dart @@ -3,7 +3,7 @@ import 'dart:typed_data'; import 'package:protobuf/protobuf.dart'; -import '../../../../veilid_support.dart'; +import '../../../veilid_support.dart'; class DHTRecord { DHTRecord( @@ -28,6 +28,7 @@ class DHTRecord { final DHTRecordCrypto _crypto; bool _open; bool _valid; + StreamSubscription? _watchSubscription; int subkeyOrDefault(int subkey) => (subkey == -1) ? _defaultSubkey : subkey; @@ -47,9 +48,8 @@ class DHTRecord { if (!_open) { return; } - final pool = await DHTRecordPool.instance(); await _routingContext.closeDHTRecord(_recordDescriptor.key); - pool.recordClosed(_recordDescriptor.key); + await DHTRecordPool.instance.recordClosed(_recordDescriptor.key); _open = false; } @@ -60,8 +60,7 @@ class DHTRecord { if (_open) { await close(); } - final pool = await DHTRecordPool.instance(); - await pool.deleteDeep(key); + await DHTRecordPool.instance.deleteDeep(key); _valid = false; } @@ -253,4 +252,20 @@ class DHTRecord { T Function(List) fromBuffer, Future Function(T) update, {int subkey = -1}) => eventualUpdateBytes(protobufUpdate(fromBuffer, update), subkey: subkey); + + Future watch( + Future Function(VeilidUpdateValueChange update) onUpdate, + {List? subkeys, + Timestamp? expiration, + int? count}) async { + // register watch with pool + _watchSubscription = await DHTRecordPool.instance.recordWatch( + _recordDescriptor.key, onUpdate, + subkeys: subkeys, expiration: expiration, count: count); + } + + Future cancelWatch() async { + // register watch with pool + await _watchSubscription?.cancel(); + } } diff --git a/packages/veilid_support/lib/dht_support/src/dht_record_cubit.dart b/packages/veilid_support/lib/dht_support/src/dht_record_cubit.dart new file mode 100644 index 0000000..494b4e0 --- /dev/null +++ b/packages/veilid_support/lib/dht_support/src/dht_record_cubit.dart @@ -0,0 +1,53 @@ +import 'package:bloc/bloc.dart'; + +import '../../veilid_support.dart'; + +class DhtRecordCubit extends Cubit> { + DhtRecordCubit({ + required DHTRecord record, + required Future Function(DHTRecord, VeilidUpdateValueChange) + stateFunction, + List watchSubkeys = const [], + }) : _record = record, + super(const AsyncValue.loading()) { + Future.delayed(Duration.zero, () async { + await record.watch((update) async { + try { + final newState = await stateFunction(record, update); + if (newState != null) { + emit(AsyncValue.data(newState)); + } + } on Exception catch (e) { + emit(AsyncValue.error(e)); + } + }, subkeys: watchSubkeys); + }); + } + + @override + Future close() async { + await _record.cancelWatch(); + await super.close(); + } + + DHTRecord _record; +} + +class SingleDHTRecordCubit extends DhtRecordCubit { + SingleDHTRecordCubit( + {required super.record, + required T? Function(List data) decodeState, + int singleSubkey = 0}) + : super( + stateFunction: (record, update) async { + // + if (update.subkeys.isNotEmpty) { + final newState = decodeState(update.valueData.data); + return newState; + } + return null; + }, + watchSubkeys: [ + ValueSubkeyRange(low: singleSubkey, high: singleSubkey) + ]); +} diff --git a/packages/veilid_support/lib/dht_support/src/dht_record_pool.dart b/packages/veilid_support/lib/dht_support/src/dht_record_pool.dart index 33960bb..937ec4b 100644 --- a/packages/veilid_support/lib/dht_support/src/dht_record_pool.dart +++ b/packages/veilid_support/lib/dht_support/src/dht_record_pool.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:mutex/mutex.dart'; @@ -35,24 +37,55 @@ class OwnedDHTRecordPointer with _$OwnedDHTRecordPointer { _$OwnedDHTRecordPointerFromJson(json as Map); } +/// Watch state +class _WatchState { + _WatchState( + {required this.subkeys, required this.expiration, required this.count}); + List? subkeys; + Timestamp? expiration; + int? count; + Timestamp? realExpiration; +} + +/// Opened DHTRecord state +class _OpenedDHTRecord { + _OpenedDHTRecord(this.routingContext) + : mutex = Mutex(), + needsWatchStateUpdate = false, + inWatchStateUpdate = false; + + Future close() async { + await watchController?.close(); + } + + Mutex mutex; + StreamController? watchController; + bool needsWatchStateUpdate; + bool inWatchStateUpdate; + _WatchState? watchState; + VeilidRoutingContext routingContext; +} + class DHTRecordPool with TableDBBacked { DHTRecordPool._(Veilid veilid, VeilidRoutingContext routingContext) : _state = DHTRecordPoolAllocations( childrenByParent: IMap(), parentByChild: IMap(), rootRecords: ISet()), - _opened = {}, + _opened = {}, _routingContext = routingContext, _veilid = veilid; // Persistent DHT record list DHTRecordPoolAllocations _state; // Which DHT records are currently open - final Map _opened; + final Map _opened; // Default routing context to use for new keys final VeilidRoutingContext _routingContext; // Convenience accessor final Veilid _veilid; + // If tick is already running or not + bool inTick = false; static DHTRecordPool? _singleton; @@ -71,37 +104,71 @@ class DHTRecordPool with TableDBBacked { Object? valueToJson(DHTRecordPoolAllocations val) => val.toJson(); ////////////////////////////////////////////////////////////// - static Mutex instanceSetupMutex = Mutex(); - // ignore: prefer_expression_function_bodies - static Future instance() async { - return instanceSetupMutex.protect(() async { - if (_singleton == null) { - final routingContext = await Veilid.instance.routingContext(); - final globalPool = DHTRecordPool._(Veilid.instance, routingContext); - globalPool._state = await globalPool.load(); - _singleton = globalPool; - } - return _singleton!; - }); + static DHTRecordPool get instance => _singleton!; + + static Future init() async { + final routingContext = await Veilid.instance.routingContext(); + final globalPool = DHTRecordPool._(Veilid.instance, routingContext); + globalPool._state = await globalPool.load(); + _singleton = globalPool; } Veilid get veilid => _veilid; - Future _recordOpened(TypedKey key) async { + Future _recordOpened( + TypedKey key, VeilidRoutingContext routingContext) async { // no race because dart is single threaded until async breaks - final m = _opened[key] ?? Mutex(); - _opened[key] = m; - await m.acquire(); - _opened[key] = m; + final odr = _opened[key] ?? _OpenedDHTRecord(routingContext); + _opened[key] = odr; + await odr.mutex.acquire(); } - void recordClosed(TypedKey key) { - final m = _opened.remove(key); - if (m == null) { + Future> recordWatch( + TypedKey key, Future Function(VeilidUpdateValueChange) onUpdate, + {required List? subkeys, + required Timestamp? expiration, + required int? count}) async { + final odr = _opened[key]; + if (odr == null) { + throw StateError("can't watch unopened record"); + } + + // Set up watch requirements + odr + ..watchState = + _WatchState(subkeys: subkeys, expiration: expiration, count: count) + ..needsWatchStateUpdate = true + ..watchController ??= + StreamController.broadcast(onCancel: () { + // Request watch state change for cancel + odr + ..watchState = null + ..needsWatchStateUpdate = true; + // If there are no more listeners then we can get rid of the controller + if (!(odr.watchController?.hasListener ?? true)) { + odr.watchController = null; + } + }); + + return odr.watchController!.stream.listen( + (update) { + Future.delayed(Duration.zero, () => onUpdate(update)); + }, + cancelOnError: true, + onError: (e) async { + await odr.watchController!.close(); + odr.watchController = null; + }); + } + + Future recordClosed(TypedKey key) async { + final odr = _opened.remove(key); + if (odr == null) { throw StateError('record already closed'); } - m.release(); + await odr.close(); + odr.mutex.release(); } Future deleteDeep(TypedKey parent) async { @@ -112,7 +179,8 @@ class DHTRecordPool with TableDBBacked { final nextDep = currentDeps.removeLast(); // Ensure we get the exclusive lock on this record - await _recordOpened(nextDep); + // Can use default routing context here because we are only deleting + await _recordOpened(nextDep, _routingContext); // Remove this child from its parent await _removeDependency(nextDep); @@ -127,7 +195,7 @@ class DHTRecordPool with TableDBBacked { final allFutures = >[]; for (final dep in allDeps) { allFutures.add(_routingContext.deleteDHTRecord(dep)); - recordClosed(dep); + await recordClosed(dep); } await Future.wait(allFutures); } @@ -220,7 +288,7 @@ class DHTRecordPool with TableDBBacked { recordDescriptor.ownerTypedKeyPair()!)); await _addDependency(parent, rec.key); - await _recordOpened(rec.key); + await _recordOpened(rec.key, dhtctx); return rec; } @@ -231,7 +299,9 @@ class DHTRecordPool with TableDBBacked { TypedKey? parent, int defaultSubkey = 0, DHTRecordCrypto? crypto}) async { - await _recordOpened(recordKey); + final dhtctx = routingContext ?? _routingContext; + + await _recordOpened(recordKey, dhtctx); late final DHTRecord rec; try { @@ -240,7 +310,6 @@ class DHTRecordPool with TableDBBacked { _validateParent(parent, recordKey); // Open from the veilid api - final dhtctx = routingContext ?? _routingContext; final recordDescriptor = await dhtctx.openDHTRecord(recordKey, null); rec = DHTRecord( routingContext: dhtctx, @@ -251,7 +320,7 @@ class DHTRecordPool with TableDBBacked { // Register the dependency await _addDependency(parent, rec.key); } on Exception catch (_) { - recordClosed(recordKey); + await recordClosed(recordKey); rethrow; } @@ -267,7 +336,9 @@ class DHTRecordPool with TableDBBacked { int defaultSubkey = 0, DHTRecordCrypto? crypto, }) async { - await _recordOpened(recordKey); + final dhtctx = routingContext ?? _routingContext; + + await _recordOpened(recordKey, dhtctx); late final DHTRecord rec; try { @@ -276,7 +347,6 @@ class DHTRecordPool with TableDBBacked { _validateParent(parent, recordKey); // Open from the veilid api - final dhtctx = routingContext ?? _routingContext; final recordDescriptor = await dhtctx.openDHTRecord(recordKey, writer); rec = DHTRecord( routingContext: dhtctx, @@ -290,7 +360,7 @@ class DHTRecordPool with TableDBBacked { // Register the dependency if specified await _addDependency(parent, rec.key); } on Exception catch (_) { - recordClosed(recordKey); + await recordClosed(recordKey); rethrow; } @@ -324,4 +394,69 @@ class DHTRecordPool with TableDBBacked { final childJson = child.toJson(); return _state.parentByChild[childJson]; } + + /// Handle the DHT record updates coming from Veilid + void processUpdateValueChange(VeilidUpdateValueChange updateValueChange) { + if (updateValueChange.subkeys.isNotEmpty) {} + } + + /// Ticker to check watch state change requests + Future tick() async { + if (inTick) { + return; + } + inTick = true; + try { + // See if any opened records need watch state changes + final unord = List>.empty(growable: true); + + for (final kv in _opened.entries) { + // Check if already updating + if (kv.value.inWatchStateUpdate) { + continue; + } + + if (kv.value.needsWatchStateUpdate) { + kv.value.inWatchStateUpdate = true; + + final ws = kv.value.watchState; + if (ws == null) { + unord.add(() async { + // Record needs watch cancel + try { + final done = + await kv.value.routingContext.cancelDHTWatch(kv.key); + assert(done, + 'should always be done when cancelling whole subkey range'); + kv.value.needsWatchStateUpdate = false; + } on VeilidAPIException { + // Failed to cancel DHT watch, try again next tick + } + kv.value.inWatchStateUpdate = false; + }()); + } else { + unord.add(() async { + // Record needs new watch + try { + final realExpiration = await kv.value.routingContext + .watchDHTValues(kv.key, + subkeys: ws.subkeys, + count: ws.count, + expiration: ws.expiration); + kv.value.needsWatchStateUpdate = false; + ws.realExpiration = realExpiration; + } on VeilidAPIException { + // Failed to cancel DHT watch, try again next tick + } + kv.value.inWatchStateUpdate = false; + }()); + } + } + } + + await unord.wait; + } finally { + inTick = false; + } + } } diff --git a/packages/veilid_support/lib/dht_support/src/dht_short_array.dart b/packages/veilid_support/lib/dht_support/src/dht_short_array.dart index f115e5a..ed917d1 100644 --- a/packages/veilid_support/lib/dht_support/src/dht_short_array.dart +++ b/packages/veilid_support/lib/dht_support/src/dht_short_array.dart @@ -69,7 +69,7 @@ class DHTShortArray { DHTRecordCrypto? crypto, KeyPair? smplWriter}) async { assert(stride <= maxElements, 'stride too long'); - final pool = await DHTRecordPool.instance(); + final pool = DHTRecordPool.instance; late final DHTRecord dhtRecord; if (smplWriter != null) { @@ -111,9 +111,7 @@ class DHTShortArray { {VeilidRoutingContext? routingContext, TypedKey? parent, DHTRecordCrypto? crypto}) async { - final pool = await DHTRecordPool.instance(); - - final dhtRecord = await pool.openRead(headRecordKey, + final dhtRecord = await DHTRecordPool.instance.openRead(headRecordKey, parent: parent, routingContext: routingContext, crypto: crypto); try { final dhtShortArray = DHTShortArray._(headRecord: dhtRecord); @@ -132,8 +130,8 @@ class DHTShortArray { TypedKey? parent, DHTRecordCrypto? crypto, }) async { - final pool = await DHTRecordPool.instance(); - final dhtRecord = await pool.openWrite(headRecordKey, writer, + final dhtRecord = await DHTRecordPool.instance.openWrite( + headRecordKey, writer, parent: parent, routingContext: routingContext, crypto: crypto); try { final dhtShortArray = DHTShortArray._(headRecord: dhtRecord); @@ -214,17 +212,15 @@ class DHTShortArray { /// Open a linked record for reading or writing, same as the head record Future _openLinkedRecord(TypedKey recordKey) async { - final pool = await DHTRecordPool.instance(); - final writer = _headRecord.writer; return (writer != null) - ? await pool.openWrite( + ? await DHTRecordPool.instance.openWrite( recordKey, writer, parent: _headRecord.key, routingContext: _headRecord.routingContext, ) - : await pool.openRead( + : await DHTRecordPool.instance.openRead( recordKey, parent: _headRecord.key, routingContext: _headRecord.routingContext, diff --git a/lib/tools/async_table_db_backed_cubit.dart b/packages/veilid_support/lib/src/async_table_db_backed_cubit.dart similarity index 89% rename from lib/tools/async_table_db_backed_cubit.dart rename to packages/veilid_support/lib/src/async_table_db_backed_cubit.dart index d8183a7..ab1c3a8 100644 --- a/lib/tools/async_table_db_backed_cubit.dart +++ b/packages/veilid_support/lib/src/async_table_db_backed_cubit.dart @@ -2,9 +2,7 @@ import 'dart:async'; import 'package:bloc/bloc.dart'; -import '../../tools/tools.dart'; -import '../init.dart'; -import '../../packages/veilid_support/veilid_support.dart'; +import '../veilid_support.dart'; abstract class AsyncTableDBBackedCubit extends Cubit> with TableDBBacked { @@ -14,7 +12,6 @@ abstract class AsyncTableDBBackedCubit extends Cubit> Future _build() async { try { - await eventualVeilid.future; emit(AsyncValue.data(await load())); } on Exception catch (e, stackTrace) { emit(AsyncValue.error(e, stackTrace)); diff --git a/lib/tools/async_value.dart b/packages/veilid_support/lib/src/async_value.dart similarity index 100% rename from lib/tools/async_value.dart rename to packages/veilid_support/lib/src/async_value.dart diff --git a/lib/tools/async_value.freezed.dart b/packages/veilid_support/lib/src/async_value.freezed.dart similarity index 100% rename from lib/tools/async_value.freezed.dart rename to packages/veilid_support/lib/src/async_value.freezed.dart diff --git a/packages/veilid_support/lib/src/identity.dart b/packages/veilid_support/lib/src/identity.dart index c4162df..5a77e2e 100644 --- a/packages/veilid_support/lib/src/identity.dart +++ b/packages/veilid_support/lib/src/identity.dart @@ -78,7 +78,7 @@ class IdentityMaster with _$IdentityMaster { extension IdentityMasterExtension on IdentityMaster { /// Deletes a master identity and the identity record under it Future delete() async { - final pool = await DHTRecordPool.instance(); + final pool = DHTRecordPool.instance; await (await pool.openRead(masterRecordKey)).delete(); } @@ -95,7 +95,7 @@ extension IdentityMasterExtension on IdentityMaster { {required SharedSecret identitySecret, required String accountKey}) async { // Read the identity key to get the account keys - final pool = await DHTRecordPool.instance(); + final pool = DHTRecordPool.instance; final identityRecordCrypto = await DHTRecordCryptoPrivate.fromSecret( identityRecordKey.kind, identitySecret); @@ -129,7 +129,7 @@ extension IdentityMasterExtension on IdentityMaster { required String accountKey, required Future Function(TypedKey parent) createAccountCallback, }) async { - final pool = await DHTRecordPool.instance(); + final pool = DHTRecordPool.instance; /////// Add account with profile to DHT @@ -186,7 +186,7 @@ class IdentityMasterWithSecrets { /// Creates a new master identity and returns it with its secrets static Future create() async { - final pool = await DHTRecordPool.instance(); + final pool = DHTRecordPool.instance; // IdentityMaster DHT record is public/unencrypted return (await pool.create(crypto: const DHTRecordCryptoPublic())) @@ -245,7 +245,7 @@ class IdentityMasterWithSecrets { /// Opens an existing master identity and validates it Future openIdentityMaster( {required TypedKey identityMasterRecordKey}) async { - final pool = await DHTRecordPool.instance(); + final pool = DHTRecordPool.instance; // IdentityMaster DHT record is public/unencrypted return (await pool.openRead(identityMasterRecordKey)) diff --git a/packages/veilid_support/lib/src/veilid_log.dart b/packages/veilid_support/lib/src/veilid_log.dart index b112292..f5db930 100644 --- a/packages/veilid_support/lib/src/veilid_log.dart +++ b/packages/veilid_support/lib/src/veilid_log.dart @@ -39,7 +39,7 @@ class VeilidLoggy implements LoggyType { Loggy get _veilidLoggy => Loggy('Veilid'); -Future processLog(VeilidLog log) async { +void processLog(VeilidLog log) { StackTrace? stackTrace; Object? error; final backtrace = log.backtrace; diff --git a/packages/veilid_support/lib/veilid_support.dart b/packages/veilid_support/lib/veilid_support.dart index f873397..fec2539 100644 --- a/packages/veilid_support/lib/veilid_support.dart +++ b/packages/veilid_support/lib/veilid_support.dart @@ -6,6 +6,7 @@ library veilid_support; export 'package:veilid/veilid.dart'; export 'dht_support/dht_support.dart'; +export 'src/async_value.dart'; export 'src/config.dart'; export 'src/identity.dart'; export 'src/json_tools.dart'; diff --git a/packages/veilid_support/pubspec.lock b/packages/veilid_support/pubspec.lock index cba74d2..d80f0e7 100644 --- a/packages/veilid_support/pubspec.lock +++ b/packages/veilid_support/pubspec.lock @@ -33,6 +33,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.11.0" + bloc: + dependency: "direct main" + description: + name: bloc + sha256: "3820f15f502372d979121de1f6b97bfcf1630ebff8fe1d52fb2b0bfa49be5b49" + url: "https://pub.dev" + source: hosted + version: "8.1.2" boolean_selector: dependency: transitive description: diff --git a/packages/veilid_support/pubspec.yaml b/packages/veilid_support/pubspec.yaml index 397b24b..44e6e08 100644 --- a/packages/veilid_support/pubspec.yaml +++ b/packages/veilid_support/pubspec.yaml @@ -5,9 +5,9 @@ version: 1.0.2+0 environment: sdk: '>=3.0.5 <4.0.0' - flutter: ">=3.10.0" dependencies: + bloc: ^8.1.2 fast_immutable_collections: ^9.1.5 freezed_annotation: ^2.2.0 json_annotation: ^4.8.1 @@ -20,7 +20,7 @@ dependencies: dev_dependencies: build_runner: ^2.4.6 - test: ^1.25.0 freezed: ^2.3.5 json_serializable: ^6.7.1 lint_hard: ^4.0.0 + test: ^1.25.0 From ba4ef05a2871d8fc80244802afc7b19fd635f7ea Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Sat, 6 Jan 2024 20:47:10 -0500 Subject: [PATCH 07/68] record pool / watch work --- lib/old_to_refactor/providers/account.dart | 2 + .../lib/dht_support/src/dht_record.dart | 48 ++++- .../lib/dht_support/src/dht_record_cubit.dart | 80 ++++--- .../lib/dht_support/src/dht_record_pool.dart | 204 +++++++----------- 4 files changed, 178 insertions(+), 156 deletions(-) diff --git a/lib/old_to_refactor/providers/account.dart b/lib/old_to_refactor/providers/account.dart index 22bdca1..3a3caa6 100644 --- a/lib/old_to_refactor/providers/account.dart +++ b/lib/old_to_refactor/providers/account.dart @@ -58,6 +58,8 @@ Future fetchAccountInfo(FetchAccountInfoRef ref, return AccountInfo(status: AccountInfoStatus.accountLocked, active: active); } +xxx login should open this key and leave it open, logout should close it + // Pull the account DHT key, decode it and return it final pool = await DHTRecordPool.instance(); final account = await (await pool.openOwned( diff --git a/packages/veilid_support/lib/dht_support/src/dht_record.dart b/packages/veilid_support/lib/dht_support/src/dht_record.dart index f810974..7c7b167 100644 --- a/packages/veilid_support/lib/dht_support/src/dht_record.dart +++ b/packages/veilid_support/lib/dht_support/src/dht_record.dart @@ -19,7 +19,10 @@ class DHTRecord { _writer = writer, _open = true, _valid = true, - _subkeySeqCache = {}; + _subkeySeqCache = {}, + needsWatchStateUpdate = false, + inWatchStateUpdate = false; + final VeilidRoutingContext _routingContext; final DHTRecordDescriptor _recordDescriptor; final int _defaultSubkey; @@ -28,7 +31,10 @@ class DHTRecord { final DHTRecordCrypto _crypto; bool _open; bool _valid; - StreamSubscription? _watchSubscription; + StreamController? watchController; + bool needsWatchStateUpdate; + bool inWatchStateUpdate; + WatchState? watchState; int subkeyOrDefault(int subkey) => (subkey == -1) ? _defaultSubkey : subkey; @@ -37,6 +43,7 @@ class DHTRecord { PublicKey get owner => _recordDescriptor.owner; KeyPair? get ownerKeyPair => _recordDescriptor.ownerKeyPair(); DHTSchema get schema => _recordDescriptor.schema; + int get subkeyCount => _recordDescriptor.schema.subkeyCount(); KeyPair? get writer => _writer; OwnedDHTRecordPointer get ownedDHTRecordPointer => OwnedDHTRecordPointer(recordKey: key, owner: ownerKeyPair!); @@ -48,8 +55,9 @@ class DHTRecord { if (!_open) { return; } + await watchController?.close(); await _routingContext.closeDHTRecord(_recordDescriptor.key); - await DHTRecordPool.instance.recordClosed(_recordDescriptor.key); + DHTRecordPool.instance.recordClosed(_recordDescriptor.key); _open = false; } @@ -258,14 +266,36 @@ class DHTRecord { {List? subkeys, Timestamp? expiration, int? count}) async { - // register watch with pool - _watchSubscription = await DHTRecordPool.instance.recordWatch( - _recordDescriptor.key, onUpdate, - subkeys: subkeys, expiration: expiration, count: count); + // Set up watch requirements which will get picked up by the next tick + watchState = + WatchState(subkeys: subkeys, expiration: expiration, count: count); + needsWatchStateUpdate = true; + } + + Future> listen( + Future Function(VeilidUpdateValueChange update) onUpdate, + ) async { + // Set up watch requirements + watchController ??= + StreamController.broadcast(onCancel: () { + // If there are no more listeners then we can get rid of the controller + watchController = null; + }); + + return watchController!.stream.listen( + (update) { + Future.delayed(Duration.zero, () => onUpdate(update)); + }, + cancelOnError: true, + onError: (e) async { + await watchController!.close(); + watchController = null; + }); } Future cancelWatch() async { - // register watch with pool - await _watchSubscription?.cancel(); + // Tear down watch requirements + watchState = null; + needsWatchStateUpdate = true; } } diff --git a/packages/veilid_support/lib/dht_support/src/dht_record_cubit.dart b/packages/veilid_support/lib/dht_support/src/dht_record_cubit.dart index 494b4e0..bd233bb 100644 --- a/packages/veilid_support/lib/dht_support/src/dht_record_cubit.dart +++ b/packages/veilid_support/lib/dht_support/src/dht_record_cubit.dart @@ -1,3 +1,6 @@ +import 'dart:async'; +import 'dart:typed_data'; + import 'package:bloc/bloc.dart'; import '../../veilid_support.dart'; @@ -5,49 +8,76 @@ import '../../veilid_support.dart'; class DhtRecordCubit extends Cubit> { DhtRecordCubit({ required DHTRecord record, - required Future Function(DHTRecord, VeilidUpdateValueChange) + required Future Function(DHTRecord) initialStateFunction, + required Future Function(DHTRecord, List, ValueData) stateFunction, - List watchSubkeys = const [], - }) : _record = record, - super(const AsyncValue.loading()) { + }) : super(const AsyncValue.loading()) { Future.delayed(Duration.zero, () async { - await record.watch((update) async { + // Make initial state update + try { + final initialState = await initialStateFunction(record); + if (initialState != null) { + emit(AsyncValue.data(initialState)); + } + } on Exception catch (e) { + emit(AsyncValue.error(e)); + } + + _subscription = await record.listen((update) async { try { - final newState = await stateFunction(record, update); + final newState = + await stateFunction(record, update.subkeys, update.valueData); if (newState != null) { emit(AsyncValue.data(newState)); } } on Exception catch (e) { emit(AsyncValue.error(e)); } - }, subkeys: watchSubkeys); + }); }); } @override Future close() async { - await _record.cancelWatch(); + await _subscription?.cancel(); + _subscription = null; await super.close(); } - DHTRecord _record; + StreamSubscription? _subscription; } -class SingleDHTRecordCubit extends DhtRecordCubit { - SingleDHTRecordCubit( - {required super.record, - required T? Function(List data) decodeState, - int singleSubkey = 0}) - : super( - stateFunction: (record, update) async { - // - if (update.subkeys.isNotEmpty) { - final newState = decodeState(update.valueData.data); - return newState; - } +// Cubit that watches the default subkey value of a dhtrecord +class DefaultDHTRecordCubit extends DhtRecordCubit { + DefaultDHTRecordCubit({ + required super.record, + required T Function(List data) decodeState, + }) : super( + initialStateFunction: (record) async { + final initialData = await record.get(); + if (initialData == null) { return null; - }, - watchSubkeys: [ - ValueSubkeyRange(low: singleSubkey, high: singleSubkey) - ]); + } + return decodeState(initialData); + }, + stateFunction: (record, subkeys, valueData) async { + final defaultSubkey = record.subkeyOrDefault(-1); + if (subkeys.containsSubkey(defaultSubkey)) { + final Uint8List data; + final firstSubkey = subkeys.firstOrNull!.low; + if (firstSubkey != defaultSubkey) { + final maybeData = await record.get(forceRefresh: true); + if (maybeData == null) { + return null; + } + data = maybeData; + } else { + data = valueData.data; + } + final newState = decodeState(data); + return newState; + } + return null; + }, + ); } diff --git a/packages/veilid_support/lib/dht_support/src/dht_record_pool.dart b/packages/veilid_support/lib/dht_support/src/dht_record_pool.dart index 937ec4b..465310b 100644 --- a/packages/veilid_support/lib/dht_support/src/dht_record_pool.dart +++ b/packages/veilid_support/lib/dht_support/src/dht_record_pool.dart @@ -2,7 +2,6 @@ import 'dart:async'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; -import 'package:mutex/mutex.dart'; import '../../../../veilid_support.dart'; @@ -38,8 +37,8 @@ class OwnedDHTRecordPointer with _$OwnedDHTRecordPointer { } /// Watch state -class _WatchState { - _WatchState( +class WatchState { + WatchState( {required this.subkeys, required this.expiration, required this.count}); List? subkeys; Timestamp? expiration; @@ -47,39 +46,20 @@ class _WatchState { Timestamp? realExpiration; } -/// Opened DHTRecord state -class _OpenedDHTRecord { - _OpenedDHTRecord(this.routingContext) - : mutex = Mutex(), - needsWatchStateUpdate = false, - inWatchStateUpdate = false; - - Future close() async { - await watchController?.close(); - } - - Mutex mutex; - StreamController? watchController; - bool needsWatchStateUpdate; - bool inWatchStateUpdate; - _WatchState? watchState; - VeilidRoutingContext routingContext; -} - class DHTRecordPool with TableDBBacked { DHTRecordPool._(Veilid veilid, VeilidRoutingContext routingContext) : _state = DHTRecordPoolAllocations( childrenByParent: IMap(), parentByChild: IMap(), rootRecords: ISet()), - _opened = {}, + _opened = {}, _routingContext = routingContext, _veilid = veilid; // Persistent DHT record list DHTRecordPoolAllocations _state; // Which DHT records are currently open - final Map _opened; + final Map _opened; // Default routing context to use for new keys final VeilidRoutingContext _routingContext; // Convenience accessor @@ -116,59 +96,18 @@ class DHTRecordPool with TableDBBacked { Veilid get veilid => _veilid; - Future _recordOpened( - TypedKey key, VeilidRoutingContext routingContext) async { - // no race because dart is single threaded until async breaks - final odr = _opened[key] ?? _OpenedDHTRecord(routingContext); - _opened[key] = odr; - await odr.mutex.acquire(); - } - - Future> recordWatch( - TypedKey key, Future Function(VeilidUpdateValueChange) onUpdate, - {required List? subkeys, - required Timestamp? expiration, - required int? count}) async { - final odr = _opened[key]; - if (odr == null) { - throw StateError("can't watch unopened record"); + void _recordOpened(DHTRecord record) { + if (_opened.containsKey(record.key)) { + throw StateError('record already opened'); } - - // Set up watch requirements - odr - ..watchState = - _WatchState(subkeys: subkeys, expiration: expiration, count: count) - ..needsWatchStateUpdate = true - ..watchController ??= - StreamController.broadcast(onCancel: () { - // Request watch state change for cancel - odr - ..watchState = null - ..needsWatchStateUpdate = true; - // If there are no more listeners then we can get rid of the controller - if (!(odr.watchController?.hasListener ?? true)) { - odr.watchController = null; - } - }); - - return odr.watchController!.stream.listen( - (update) { - Future.delayed(Duration.zero, () => onUpdate(update)); - }, - cancelOnError: true, - onError: (e) async { - await odr.watchController!.close(); - odr.watchController = null; - }); + _opened[record.key] = record; } - Future recordClosed(TypedKey key) async { - final odr = _opened.remove(key); - if (odr == null) { + void recordClosed(TypedKey key) { + final rec = _opened.remove(key); + if (rec == null) { throw StateError('record already closed'); } - await odr.close(); - odr.mutex.release(); } Future deleteDeep(TypedKey parent) async { @@ -178,10 +117,6 @@ class DHTRecordPool with TableDBBacked { while (currentDeps.isNotEmpty) { final nextDep = currentDeps.removeLast(); - // Ensure we get the exclusive lock on this record - // Can use default routing context here because we are only deleting - await _recordOpened(nextDep, _routingContext); - // Remove this child from its parent await _removeDependency(nextDep); @@ -191,11 +126,16 @@ class DHTRecordPool with TableDBBacked { currentDeps.addAll(childDeps); } - // Delete all records + // Delete all dependent records in parallel final allFutures = >[]; for (final dep in allDeps) { + // If record is opened, close it first + final rec = _opened[dep]; + if (rec != null) { + await rec.close(); + } + // Then delete allFutures.add(_routingContext.deleteDHTRecord(dep)); - await recordClosed(dep); } await Future.wait(allFutures); } @@ -288,7 +228,8 @@ class DHTRecordPool with TableDBBacked { recordDescriptor.ownerTypedKeyPair()!)); await _addDependency(parent, rec.key); - await _recordOpened(rec.key, dhtctx); + + _recordOpened(rec); return rec; } @@ -301,28 +242,22 @@ class DHTRecordPool with TableDBBacked { DHTRecordCrypto? crypto}) async { final dhtctx = routingContext ?? _routingContext; - await _recordOpened(recordKey, dhtctx); - late final DHTRecord rec; - try { - // If we are opening a key that already exists - // make sure we are using the same parent if one was specified - _validateParent(parent, recordKey); + // If we are opening a key that already exists + // make sure we are using the same parent if one was specified + _validateParent(parent, recordKey); - // Open from the veilid api - final recordDescriptor = await dhtctx.openDHTRecord(recordKey, null); - rec = DHTRecord( - routingContext: dhtctx, - recordDescriptor: recordDescriptor, - defaultSubkey: defaultSubkey, - crypto: crypto ?? const DHTRecordCryptoPublic()); + // Open from the veilid api + final recordDescriptor = await dhtctx.openDHTRecord(recordKey, null); + rec = DHTRecord( + routingContext: dhtctx, + recordDescriptor: recordDescriptor, + defaultSubkey: defaultSubkey, + crypto: crypto ?? const DHTRecordCryptoPublic()); - // Register the dependency - await _addDependency(parent, rec.key); - } on Exception catch (_) { - await recordClosed(recordKey); - rethrow; - } + // Register the dependency + await _addDependency(parent, rec.key); + _recordOpened(rec); return rec; } @@ -338,31 +273,25 @@ class DHTRecordPool with TableDBBacked { }) async { final dhtctx = routingContext ?? _routingContext; - await _recordOpened(recordKey, dhtctx); - late final DHTRecord rec; - try { - // If we are opening a key that already exists - // make sure we are using the same parent if one was specified - _validateParent(parent, recordKey); + // If we are opening a key that already exists + // make sure we are using the same parent if one was specified + _validateParent(parent, recordKey); - // Open from the veilid api - final recordDescriptor = await dhtctx.openDHTRecord(recordKey, writer); - rec = DHTRecord( - routingContext: dhtctx, - recordDescriptor: recordDescriptor, - defaultSubkey: defaultSubkey, - writer: writer, - crypto: crypto ?? - await DHTRecordCryptoPrivate.fromTypedKeyPair( - TypedKeyPair.fromKeyPair(recordKey.kind, writer))); + // Open from the veilid api + final recordDescriptor = await dhtctx.openDHTRecord(recordKey, writer); + rec = DHTRecord( + routingContext: dhtctx, + recordDescriptor: recordDescriptor, + defaultSubkey: defaultSubkey, + writer: writer, + crypto: crypto ?? + await DHTRecordCryptoPrivate.fromTypedKeyPair( + TypedKeyPair.fromKeyPair(recordKey.kind, writer))); - // Register the dependency if specified - await _addDependency(parent, rec.key); - } on Exception catch (_) { - await recordClosed(recordKey); - rethrow; - } + // Register the dependency if specified + await _addDependency(parent, rec.key); + _recordOpened(rec); return rec; } @@ -389,15 +318,46 @@ class DHTRecordPool with TableDBBacked { crypto: crypto, ); + /// Look up an opened DHRRecord + DHTRecord? getOpenedRecord(TypedKey recordKey) => _opened[recordKey]; + /// Get the parent of a DHTRecord key if it exists - TypedKey? getParentRecord(TypedKey child) { + TypedKey? getParentRecordKey(TypedKey child) { final childJson = child.toJson(); return _state.parentByChild[childJson]; } /// Handle the DHT record updates coming from Veilid void processUpdateValueChange(VeilidUpdateValueChange updateValueChange) { - if (updateValueChange.subkeys.isNotEmpty) {} + if (updateValueChange.subkeys.isNotEmpty) { + // Change + for (final kv in _opened.entries) { + if (kv.key == updateValueChange.key) { + kv.value.watchController?.add(updateValueChange); + break; + } + } + } else { + // Expired, process renewal if desired + for (final kv in _opened.entries) { + if (kv.key == updateValueChange.key) { + // Renew watch state + kv.value.needsWatchStateUpdate = true; + + // See if the watch had an expiration and if it has expired + // otherwise the renewal will keep the same parameters + final watchState = kv.value.watchState; + if (watchState != null) { + final exp = watchState.expiration; + if (exp != null && exp.value < Veilid.instance.now().value) { + // Has expiration, and it has expired, clear watch state + kv.value.watchState = null; + } + } + break; + } + } + } } /// Ticker to check watch state change requests From b83aa3a64b1ceee52ba7522da94ba6810ded19e7 Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Mon, 8 Jan 2024 21:37:08 -0500 Subject: [PATCH 08/68] more refactor --- lib/account_manager/account_manager.dart | 2 +- .../active_user_login_cubit.dart | 3 +- .../local_accounts_cubit.dart | 3 +- .../user_logins_cubit/user_logins_cubit.dart | 3 +- lib/account_manager/models/account_info.dart | 22 +++ .../models/active_account_info.dart | 26 +++ lib/account_manager/models/models.dart | 2 + .../account_repository.dart | 161 +++++++++++++++++- .../new_account_page/new_account_page.dart | 0 .../views}/profile_widget.dart | 7 +- .../{view/view.dart => views/views.dart} | 1 + lib/app.dart | 14 +- lib/layout/home.dart | 91 +++++----- lib/layout/layout.dart | 8 +- lib/layout/main_pager/main_pager.dart | 2 +- lib/layout/settings.dart | 139 --------------- lib/main.dart | 13 +- lib/old_to_refactor/entities/entities.dart | 3 - lib/old_to_refactor/providers/account.dart | 147 ---------------- lib/router/cubit/router_cubit.dart | 11 +- lib/settings/models/models.dart | 1 + .../models}/preferences.dart | 15 ++ .../models}/preferences.freezed.dart | 27 ++- lib/settings/models/preferences.g.dart | 36 ++++ lib/settings/preferences_cubit.dart | 9 + lib/settings/preferences_repository.dart | 34 ++++ lib/settings/settings.dart | 4 + lib/settings/settings_page.dart | 129 ++++++++++++++ lib/theme/models/models.dart | 2 +- lib/theme/models/theme_preference.dart | 62 +++++++ lib/theme/repository/theme_repository.dart | 131 -------------- lib/theme/theme.dart | 1 - lib/tools/shared_preferences.dart | 80 +++++++++ lib/tools/tools.dart | 1 + lib/tools/widget_helpers.dart | 23 +++ lib/tools/window_control.dart | 10 +- .../lib/dht_support/src/dht_record.dart | 1 - .../lib/dht_support/src/dht_record_cubit.dart | 6 +- packages/veilid_support/lib/src/table_db.dart | 6 +- 39 files changed, 722 insertions(+), 514 deletions(-) create mode 100644 lib/account_manager/models/account_info.dart create mode 100644 lib/account_manager/models/active_account_info.dart rename lib/account_manager/{view => views}/new_account_page/new_account_page.dart (100%) rename lib/{old_to_refactor/components => account_manager/views}/profile_widget.dart (86%) rename lib/account_manager/{view/view.dart => views/views.dart} (62%) delete mode 100644 lib/layout/settings.dart delete mode 100644 lib/old_to_refactor/entities/entities.dart delete mode 100644 lib/old_to_refactor/providers/account.dart create mode 100644 lib/settings/models/models.dart rename lib/{old_to_refactor/entities => settings/models}/preferences.dart (75%) rename lib/{old_to_refactor/entities => settings/models}/preferences.freezed.dart (94%) create mode 100644 lib/settings/models/preferences.g.dart create mode 100644 lib/settings/preferences_cubit.dart create mode 100644 lib/settings/preferences_repository.dart create mode 100644 lib/settings/settings.dart create mode 100644 lib/settings/settings_page.dart delete mode 100644 lib/theme/repository/theme_repository.dart create mode 100644 lib/tools/shared_preferences.dart diff --git a/lib/account_manager/account_manager.dart b/lib/account_manager/account_manager.dart index d04e6f0..f4b3f22 100644 --- a/lib/account_manager/account_manager.dart +++ b/lib/account_manager/account_manager.dart @@ -1,3 +1,3 @@ export 'cubit/cubit.dart'; export 'repository/repository.dart'; -export 'view/view.dart'; +export 'views/views.dart'; diff --git a/lib/account_manager/cubit/active_user_login_cubit/active_user_login_cubit.dart b/lib/account_manager/cubit/active_user_login_cubit/active_user_login_cubit.dart index ec0aaf5..70392f5 100644 --- a/lib/account_manager/cubit/active_user_login_cubit/active_user_login_cubit.dart +++ b/lib/account_manager/cubit/active_user_login_cubit/active_user_login_cubit.dart @@ -16,8 +16,7 @@ class ActiveUserLoginCubit extends Cubit { } void _initAccountRepositorySubscription() { - _accountRepositorySubscription = - _accountRepository.changes().listen((change) { + _accountRepositorySubscription = _accountRepository.stream.listen((change) { switch (change) { case AccountRepositoryChange.activeUserLogin: emit(_accountRepository.getActiveUserLogin()); diff --git a/lib/account_manager/cubit/local_accounts_cubit/local_accounts_cubit.dart b/lib/account_manager/cubit/local_accounts_cubit/local_accounts_cubit.dart index d885a7e..ee9aa81 100644 --- a/lib/account_manager/cubit/local_accounts_cubit/local_accounts_cubit.dart +++ b/lib/account_manager/cubit/local_accounts_cubit/local_accounts_cubit.dart @@ -18,8 +18,7 @@ class LocalAccountsCubit extends Cubit { } void _initAccountRepositorySubscription() { - _accountRepositorySubscription = - _accountRepository.changes().listen((change) { + _accountRepositorySubscription = _accountRepository.stream.listen((change) { switch (change) { case AccountRepositoryChange.localAccounts: emit(_accountRepository.getLocalAccounts()); diff --git a/lib/account_manager/cubit/user_logins_cubit/user_logins_cubit.dart b/lib/account_manager/cubit/user_logins_cubit/user_logins_cubit.dart index fd2f4ff..9e7aee1 100644 --- a/lib/account_manager/cubit/user_logins_cubit/user_logins_cubit.dart +++ b/lib/account_manager/cubit/user_logins_cubit/user_logins_cubit.dart @@ -18,8 +18,7 @@ class UserLoginsCubit extends Cubit { } void _initAccountRepositorySubscription() { - _accountRepositorySubscription = - _accountRepository.changes().listen((change) { + _accountRepositorySubscription = _accountRepository.stream.listen((change) { switch (change) { case AccountRepositoryChange.userLogins: emit(_accountRepository.getUserLogins()); diff --git a/lib/account_manager/models/account_info.dart b/lib/account_manager/models/account_info.dart new file mode 100644 index 0000000..14b25dc --- /dev/null +++ b/lib/account_manager/models/account_info.dart @@ -0,0 +1,22 @@ +import 'package:meta/meta.dart'; +import 'package:veilid_support/veilid_support.dart'; + +enum AccountInfoStatus { + noAccount, + accountInvalid, + accountLocked, + accountReady, +} + +@immutable +class AccountInfo { + const AccountInfo({ + required this.status, + required this.active, + this.accountRecord, + }); + + final AccountInfoStatus status; + final bool active; + final DHTRecord? accountRecord; +} diff --git a/lib/account_manager/models/active_account_info.dart b/lib/account_manager/models/active_account_info.dart new file mode 100644 index 0000000..b20dd6e --- /dev/null +++ b/lib/account_manager/models/active_account_info.dart @@ -0,0 +1,26 @@ +import 'package:meta/meta.dart'; +import 'package:veilid_support/veilid_support.dart'; + +import 'local_account/local_account.dart'; +import 'user_login/user_login.dart'; + +@immutable +class ActiveAccountInfo { + const ActiveAccountInfo({ + required this.localAccount, + required this.userLogin, + required this.accountRecord, + }); + // + + KeyPair getConversationWriter() { + final identityKey = localAccount.identityMaster.identityPublicKey; + final identitySecret = userLogin.identitySecret; + return KeyPair(key: identityKey, secret: identitySecret.value); + } + + // + final LocalAccount localAccount; + final UserLogin userLogin; + final DHTRecord accountRecord; +} diff --git a/lib/account_manager/models/models.dart b/lib/account_manager/models/models.dart index 320e917..d4b0ab5 100644 --- a/lib/account_manager/models/models.dart +++ b/lib/account_manager/models/models.dart @@ -1,3 +1,5 @@ +export 'account_info.dart'; +export 'active_account_info.dart'; export 'encryption_key_type.dart'; export 'local_account/local_account.dart'; export 'new_profile_spec.dart'; diff --git a/lib/account_manager/repository/account_repository/account_repository.dart b/lib/account_manager/repository/account_repository/account_repository.dart index 2be5565..3132923 100644 --- a/lib/account_manager/repository/account_repository/account_repository.dart +++ b/lib/account_manager/repository/account_repository/account_repository.dart @@ -1,7 +1,10 @@ +import 'dart:async'; + import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:veilid_support/veilid_support.dart'; import '../../../../proto/proto.dart' as proto; +import '../../../tools/tools.dart'; import '../../models/models.dart'; import 'active_logins.dart'; @@ -12,7 +15,9 @@ enum AccountRepositoryChange { localAccounts, userLogins, activeUserLogin } class AccountRepository { AccountRepository._() : _localAccounts = _initLocalAccounts(), - _activeLogins = _initActiveLogins(); + _activeLogins = _initActiveLogins(), + _streamController = + StreamController.broadcast(); static TableDBValue> _initLocalAccounts() => TableDBValue( tableName: 'local_account_manager', @@ -33,6 +38,7 @@ class AccountRepository { final TableDBValue> _localAccounts; final TableDBValue _activeLogins; + final StreamController _streamController; ////////////////////////////////////////////////////////////// /// Singleton initialization @@ -42,12 +48,13 @@ class AccountRepository { Future init() async { await _localAccounts.load(); await _activeLogins.load(); + await _openLoggedInDHTRecords(); } ////////////////////////////////////////////////////////////// /// Streams - Stream changes() async* {} + Stream get stream => _streamController.stream; ////////////////////////////////////////////////////////////// /// Selectors @@ -75,6 +82,84 @@ class AccountRepository { return userLogins[idx]; } + AccountInfo getAccountInfo({required TypedKey accountMasterRecordKey}) { + // Get which local account we want to fetch the profile for + final localAccount = + fetchLocalAccount(accountMasterRecordKey: accountMasterRecordKey); + if (localAccount == null) { + // Local account does not exist + return const AccountInfo( + status: AccountInfoStatus.noAccount, active: false); + } + + // See if we've logged into this account or if it is locked + final activeUserLogin = getActiveUserLogin(); + final active = activeUserLogin == accountMasterRecordKey; + + final login = + fetchUserLogin(accountMasterRecordKey: accountMasterRecordKey); + if (login == null) { + // Account was locked + return AccountInfo( + status: AccountInfoStatus.accountLocked, active: active); + } + + // Pull the account DHT key, decode it and return it + final pool = DHTRecordPool.instance; + final accountRecord = + pool.getOpenedRecord(login.accountRecordInfo.accountRecord.recordKey); + if (accountRecord == null) { + // Account could not be read or decrypted from DHT + return AccountInfo( + status: AccountInfoStatus.accountInvalid, active: active); + } + + // Got account, decrypted and decoded + return AccountInfo( + status: AccountInfoStatus.accountReady, + active: active, + accountRecord: accountRecord); + } + + Future fetchActiveAccountInfo() async { + // See if we've logged into this account or if it is locked + final activeUserLogin = getActiveUserLogin(); + if (activeUserLogin == null) { + // No user logged in + return null; + } + + // Get the user login + final userLogin = fetchUserLogin(accountMasterRecordKey: activeUserLogin); + if (userLogin == null) { + // Account was locked + return null; + } + + // Get which local account we want to fetch the profile for + final localAccount = + fetchLocalAccount(accountMasterRecordKey: activeUserLogin); + if (localAccount == null) { + // Local account does not exist + return null; + } + + // Pull the account DHT key, decode it and return it + final pool = DHTRecordPool.instance; + final accountRecord = pool + .getOpenedRecord(userLogin.accountRecordInfo.accountRecord.recordKey); + if (accountRecord == null) { + return null; + } + + // Got account, decrypted and decoded + return ActiveAccountInfo( + localAccount: localAccount, + userLogin: userLogin, + accountRecord: accountRecord, + ); + } + ////////////////////////////////////////////////////////////// /// Mutators @@ -86,6 +171,7 @@ class AccountRepository { .removeAt(oldIndex, removedItem) .insert(newIndex, removedItem.value!); await _localAccounts.set(updated); + _streamController.add(AccountRepositoryChange.localAccounts); } /// Creates a new master identity, an account associated with the master @@ -172,6 +258,7 @@ class AccountRepository { final newLocalAccounts = localAccounts.add(localAccount); await _localAccounts.set(newLocalAccounts); + _streamController.add(AccountRepositoryChange.localAccounts); // Return local account object return localAccount; @@ -186,6 +273,7 @@ class AccountRepository { (la) => la.identityMaster.masterRecordKey == accountMasterRecordKey); await _localAccounts.set(newLocalAccounts); + _streamController.add(AccountRepositoryChange.localAccounts); // TO DO: wipe messages @@ -201,6 +289,11 @@ class AccountRepository { Future switchToAccount(TypedKey? accountMasterRecordKey) async { final activeLogins = await _activeLogins.get(); + if (activeLogins.activeUserLogin == accountMasterRecordKey) { + // Nothing to do + return; + } + if (accountMasterRecordKey != null) { // Assert the specified record key can be found, will throw if not final _ = activeLogins.userLogins.firstWhere( @@ -209,6 +302,7 @@ class AccountRepository { final newActiveLogins = activeLogins.copyWith(activeUserLogin: accountMasterRecordKey); await _activeLogins.set(newActiveLogins); + _streamController.add(AccountRepositoryChange.activeUserLogin); } Future _decryptedLogin( @@ -242,6 +336,12 @@ class AccountRepository { addIfNotFound: true), activeUserLogin: identityMaster.masterRecordKey); await _activeLogins.set(newActiveLogins); + _streamController + ..add(AccountRepositoryChange.activeUserLogin) + ..add(AccountRepositoryChange.userLogins); + + // Ensure all logins are opened + await _openLoggedInDHTRecords(); return true; } @@ -273,11 +373,25 @@ class AccountRepository { } Future logout(TypedKey? accountMasterRecordKey) async { + // Resolve which user to log out final activeLogins = await _activeLogins.get(); final logoutUser = accountMasterRecordKey ?? activeLogins.activeUserLogin; if (logoutUser == null) { + log.error('missing user in logout: $accountMasterRecordKey'); return; } + + final logoutUserLogin = fetchUserLogin(accountMasterRecordKey: logoutUser); + if (logoutUserLogin != null) { + // Close DHT records for this account + final pool = DHTRecordPool.instance; + final accountRecordKey = + logoutUserLogin.accountRecordInfo.accountRecord.recordKey; + final accountRecord = pool.getOpenedRecord(accountRecordKey); + await accountRecord?.close(); + } + + // Remove user from active logins list final newActiveLogins = activeLogins.copyWith( activeUserLogin: activeLogins.activeUserLogin == logoutUser ? null @@ -285,5 +399,48 @@ class AccountRepository { userLogins: activeLogins.userLogins .removeWhere((ul) => ul.accountMasterRecordKey == logoutUser)); await _activeLogins.set(newActiveLogins); + if (activeLogins.activeUserLogin == logoutUser) { + _streamController.add(AccountRepositoryChange.activeUserLogin); + } + _streamController.add(AccountRepositoryChange.userLogins); + } + + Future _openLoggedInDHTRecords() async { + final pool = DHTRecordPool.instance; + + // For all user logins if they arent open yet + final activeLogins = await _activeLogins.get(); + for (final userLogin in activeLogins.userLogins) { + final accountRecordKey = + userLogin.accountRecordInfo.accountRecord.recordKey; + final existingAccountRecord = pool.getOpenedRecord(accountRecordKey); + if (existingAccountRecord != null) { + continue; + } + final localAccount = fetchLocalAccount( + accountMasterRecordKey: userLogin.accountMasterRecordKey); + + // Record not yet open, do it + final record = await pool.openOwned( + userLogin.accountRecordInfo.accountRecord, + parent: localAccount!.identityMaster.identityRecordKey); + // Watch the record's only (default) key + await record.watch(); + + // .scope( + // (accountRec) => accountRec.getProtobuf(proto.Account.fromBuffer)); + } + } + + Future _closeLoggedInDHTRecords() async { + final pool = DHTRecordPool.instance; + + final activeLogins = await _activeLogins.get(); + for (final userLogin in activeLogins.userLogins) { + final accountRecordKey = + userLogin.accountRecordInfo.accountRecord.recordKey; + final accountRecord = pool.getOpenedRecord(accountRecordKey); + await accountRecord?.close(); + } } } diff --git a/lib/account_manager/view/new_account_page/new_account_page.dart b/lib/account_manager/views/new_account_page/new_account_page.dart similarity index 100% rename from lib/account_manager/view/new_account_page/new_account_page.dart rename to lib/account_manager/views/new_account_page/new_account_page.dart diff --git a/lib/old_to_refactor/components/profile_widget.dart b/lib/account_manager/views/profile_widget.dart similarity index 86% rename from lib/old_to_refactor/components/profile_widget.dart rename to lib/account_manager/views/profile_widget.dart index a4a7090..1ca56ca 100644 --- a/lib/old_to_refactor/components/profile_widget.dart +++ b/lib/account_manager/views/profile_widget.dart @@ -1,11 +1,10 @@ import 'package:awesome_extensions/awesome_extensions.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import '../tools/tools.dart'; +import '../../theme/theme.dart'; -class ProfileWidget extends ConsumerWidget { +class ProfileWidget extends StatelessWidget { const ProfileWidget({ required this.name, this.pronouns, @@ -17,7 +16,7 @@ class ProfileWidget extends ConsumerWidget { @override // ignore: prefer_expression_function_bodies - Widget build(BuildContext context, WidgetRef ref) { + Widget build(BuildContext context) { final theme = Theme.of(context); final scale = theme.extension()!; final textTheme = theme.textTheme; diff --git a/lib/account_manager/view/view.dart b/lib/account_manager/views/views.dart similarity index 62% rename from lib/account_manager/view/view.dart rename to lib/account_manager/views/views.dart index 3304b76..a10db1b 100644 --- a/lib/account_manager/view/view.dart +++ b/lib/account_manager/views/views.dart @@ -1 +1,2 @@ export 'new_account_page/new_account_page.dart'; +export 'profile_widget.dart'; diff --git a/lib/app.dart b/lib/app.dart index 903d4f9..7d4001d 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -8,24 +8,25 @@ import 'package:form_builder_validators/form_builder_validators.dart'; import 'account_manager/account_manager.dart'; import 'router/router.dart'; +import 'settings/settings.dart'; import 'tick.dart'; class VeilidChatApp extends StatelessWidget { const VeilidChatApp({ - required this.themeData, + required this.initialThemeData, super.key, }); static const String name = 'VeilidChat'; - final ThemeData themeData; + final ThemeData initialThemeData; @override Widget build(BuildContext context) { final localizationDelegate = LocalizedApp.of(context).delegate; return ThemeProvider( - initTheme: themeData, + initTheme: initialThemeData, builder: (_, theme) => LocalizationProvider( state: LocalizationProvider.of(context).state, child: MultiBlocProvider( @@ -46,6 +47,10 @@ class VeilidChatApp extends StatelessWidget { create: (context) => ActiveUserLoginCubit(AccountRepository.instance), ), + BlocProvider( + create: (context) => + PreferencesCubit(PreferencesRepository.instance), + ) ], child: BackgroundTicker( builder: (context) => MaterialApp.router( @@ -70,6 +75,7 @@ class VeilidChatApp extends StatelessWidget { @override void debugFillProperties(DiagnosticPropertiesBuilder properties) { super.debugFillProperties(properties); - properties.add(DiagnosticsProperty('themeData', themeData)); + properties + .add(DiagnosticsProperty('themeData', initialThemeData)); } } diff --git a/lib/layout/home.dart b/lib/layout/home.dart index a78ebb9..86e838f 100644 --- a/lib/layout/home.dart +++ b/lib/layout/home.dart @@ -5,9 +5,11 @@ import 'package:flutter_animate/flutter_animate.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_translate/flutter_translate.dart'; import 'package:go_router/go_router.dart'; +import 'package:veilid_support/veilid_support.dart'; -import '../account_manager/account_manager.dart'; import '../../proto/proto.dart' as proto; +import '../account_manager/account_manager.dart'; +import '../account_manager/models/models.dart'; import '../theme/theme.dart'; import '../tools/tools.dart'; import 'main_pager/main_pager.dart'; @@ -92,36 +94,48 @@ class HomePageState extends State with TickerProviderStateMixin { BuildContext context, IList localAccounts, TypedKey activeUserLogin, - proto.Account account) { + DHTRecord accountRecord) { final theme = Theme.of(context); final scale = theme.extension()!; - return Column(children: [ - Row(children: [ - IconButton( - icon: const Icon(Icons.settings), - color: scale.secondaryScale.text, - constraints: const BoxConstraints.expand(height: 64, width: 64), - style: ButtonStyle( - backgroundColor: - MaterialStateProperty.all(scale.secondaryScale.border), - shape: MaterialStateProperty.all(const RoundedRectangleBorder( - borderRadius: BorderRadius.all(Radius.circular(16))))), - tooltip: translate('app_bar.settings_tooltip'), - onPressed: () async { - context.go('/home/settings'); - }).paddingLTRB(0, 0, 8, 0), - ProfileWidget( - name: account.profile.name, - pronouns: account.profile.pronouns, - ).expanded(), - ]).paddingAll(8), - MainPager( - localAccounts: localAccounts, - activeUserLogin: activeUserLogin, - account: account) - .expanded() - ]); + return BlocProvider( + create: (context) => DefaultDHTRecordCubit( + record: accountRecord, decodeState: proto.Account.fromBuffer), + child: Column(children: [ + Row(children: [ + IconButton( + icon: const Icon(Icons.settings), + color: scale.secondaryScale.text, + constraints: const BoxConstraints.expand(height: 64, width: 64), + style: ButtonStyle( + backgroundColor: + MaterialStateProperty.all(scale.secondaryScale.border), + shape: MaterialStateProperty.all( + const RoundedRectangleBorder( + borderRadius: + BorderRadius.all(Radius.circular(16))))), + tooltip: translate('app_bar.settings_tooltip'), + onPressed: () async { + context.go('/home/settings'); + }).paddingLTRB(0, 0, 8, 0), + context + .watch>() + .state + .builder((context, account) => ProfileWidget( + name: account.profile.name, + pronouns: account.profile.pronouns, + )) + .expanded(), + ]).paddingAll(8), + context + .watch>() + .state + .builder((context, account) => MainPager( + localAccounts: localAccounts, + activeUserLogin: activeUserLogin, + account: account)) + .expanded() + ])); } Widget buildUserPanel() => Builder(builder: (context) { @@ -133,12 +147,9 @@ class HomePageState extends State with TickerProviderStateMixin { return waitingPage(context); } - final accountV = ref.watch( - fetchAccountProvider(accountMasterRecordKey: activeUserLogin)); - if (!accountV.hasValue) { - return waitingPage(context); - } - final account = accountV.requireValue; + final account = AccountRepository.instance + .getAccountInfo(accountMasterRecordKey: activeUserLogin); + switch (account.status) { case AccountInfoStatus.noAccount: Future.delayed(0.ms, () async { @@ -147,11 +158,10 @@ class HomePageState extends State with TickerProviderStateMixin { translate('home.missing_account_title'), translate('home.missing_account_text')); // Delete account - await ref - .read(localAccountsProvider.notifier) + await AccountRepository.instance .deleteLocalAccount(activeUserLogin); // Switch to no active user login - await ref.read(loginsProvider.notifier).switchToAccount(null); + await AccountRepository.instance.switchToAccount(null); }); return waitingPage(context); case AccountInfoStatus.accountInvalid: @@ -161,11 +171,10 @@ class HomePageState extends State with TickerProviderStateMixin { translate('home.invalid_account_title'), translate('home.invalid_account_text')); // Delete account - await ref - .read(localAccountsProvider.notifier) + await AccountRepository.instance .deleteLocalAccount(activeUserLogin); // Switch to no active user login - await ref.read(loginsProvider.notifier).switchToAccount(null); + await AccountRepository.instance.switchToAccount(null); }); return waitingPage(context); case AccountInfoStatus.accountLocked: @@ -176,7 +185,7 @@ class HomePageState extends State with TickerProviderStateMixin { context, localAccounts, activeUserLogin, - account.account!, + account.accountRecord!, ); } }); diff --git a/lib/layout/layout.dart b/lib/layout/layout.dart index 8b13789..34b7364 100644 --- a/lib/layout/layout.dart +++ b/lib/layout/layout.dart @@ -1 +1,7 @@ - +export 'chat_only.dart'; +export 'default_app_bar.dart'; +export 'edit_account.dart'; +export 'edit_contact.dart'; +export 'home.dart'; +export 'index.dart'; +export 'main_pager/main_pager.dart'; diff --git a/lib/layout/main_pager/main_pager.dart b/lib/layout/main_pager/main_pager.dart index 971ec1f..37c55fd 100644 --- a/lib/layout/main_pager/main_pager.dart +++ b/lib/layout/main_pager/main_pager.dart @@ -24,7 +24,7 @@ import '../../../../packages/veilid_support/veilid_support.dart'; import 'account.dart'; import 'chats.dart'; -class MainPager extends ConsumerStatefulWidget { +class MainPager extends StatefulWidget { const MainPager( {required this.localAccounts, required this.activeUserLogin, diff --git a/lib/layout/settings.dart b/lib/layout/settings.dart deleted file mode 100644 index fb40352..0000000 --- a/lib/layout/settings.dart +++ /dev/null @@ -1,139 +0,0 @@ -import 'package:animated_theme_switcher/animated_theme_switcher.dart'; -import 'package:awesome_extensions/awesome_extensions.dart'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_form_builder/flutter_form_builder.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:flutter_translate/flutter_translate.dart'; - -import '../../components/default_app_bar.dart'; -import '../../components/signal_strength_meter.dart'; -import '../../entities/preferences.dart'; -import '../providers/window_control.dart'; -import '../../tools/tools.dart'; - -class SettingsPage extends StatefulWidget { - const SettingsPage({super.key}); - - @override - SettingsPageState createState() => SettingsPageState(); -} - -class SettingsPageState extends ConsumerState { - final _formKey = GlobalKey(); - late bool isInAsyncCall = false; -// ThemePreferences? themePreferences; - static const String formFieldTheme = 'theme'; - static const String formFieldBrightness = 'brightness'; - // static const String formFieldTitle = 'title'; - - @override - void initState() { - super.initState(); - } - - List> _getThemeDropdownItems() { - const colorPrefs = ColorPreference.values; - final colorNames = { - ColorPreference.scarlet: translate('themes.scarlet'), - ColorPreference.vapor: translate('themes.vapor'), - ColorPreference.babydoll: translate('themes.babydoll'), - ColorPreference.gold: translate('themes.gold'), - ColorPreference.garden: translate('themes.garden'), - ColorPreference.forest: translate('themes.forest'), - ColorPreference.arctic: translate('themes.arctic'), - ColorPreference.lapis: translate('themes.lapis'), - ColorPreference.eggplant: translate('themes.eggplant'), - ColorPreference.lime: translate('themes.lime'), - ColorPreference.grim: translate('themes.grim'), - ColorPreference.contrast: translate('themes.contrast') - }; - - return colorPrefs - .map((e) => DropdownMenuItem(value: e, child: Text(colorNames[e]!))) - .toList(); - } - - List> _getBrightnessDropdownItems() { - const brightnessPrefs = BrightnessPreference.values; - final brightnessNames = { - BrightnessPreference.system: translate('brightness.system'), - BrightnessPreference.light: translate('brightness.light'), - BrightnessPreference.dark: translate('brightness.dark') - }; - - return brightnessPrefs - .map( - (e) => DropdownMenuItem(value: e, child: Text(brightnessNames[e]!))) - .toList(); - } - - @override - Widget build(BuildContext context) { - ref.watch(windowControlProvider); - final themeService = ref.watch(themeServiceProvider).valueOrNull; - if (themeService == null) { - return waitingPage(context); - } - final themePreferences = themeService.load(); - - return ThemeSwitchingArea( - child: Scaffold( - // resizeToAvoidBottomInset: false, - appBar: DefaultAppBar( - title: Text(translate('settings_page.titlebar')), - leading: IconButton( - icon: const Icon(Icons.arrow_back), - onPressed: () => context.pop(), - ), - actions: [ - const SignalStrengthMeterWidget().paddingLTRB(16, 0, 16, 0), - ]), - - body: FormBuilder( - key: _formKey, - child: ListView( - children: [ - ThemeSwitcher.withTheme( - builder: (_, switcher, theme) => FormBuilderDropdown( - name: formFieldTheme, - decoration: InputDecoration( - label: Text(translate('settings_page.color_theme'))), - items: _getThemeDropdownItems(), - initialValue: themePreferences.colorPreference, - onChanged: (value) async { - final newPrefs = themePreferences.copyWith( - colorPreference: value as ColorPreference); - await themeService.save(newPrefs); - switcher.changeTheme(theme: themeService.get(newPrefs)); - ref.invalidate(themeServiceProvider); - setState(() {}); - })), - ThemeSwitcher.withTheme( - builder: (_, switcher, theme) => FormBuilderDropdown( - name: formFieldBrightness, - decoration: InputDecoration( - label: - Text(translate('settings_page.brightness_mode'))), - items: _getBrightnessDropdownItems(), - initialValue: themePreferences.brightnessPreference, - onChanged: (value) async { - final newPrefs = themePreferences.copyWith( - brightnessPreference: value as BrightnessPreference); - await themeService.save(newPrefs); - switcher.changeTheme(theme: themeService.get(newPrefs)); - ref.invalidate(themeServiceProvider); - setState(() {}); - })), - ], - ), - ).paddingSymmetric(horizontal: 24, vertical: 8), - )); - } - - @override - void debugFillProperties(DiagnosticPropertiesBuilder properties) { - super.debugFillProperties(properties); - properties.add(DiagnosticsProperty('isInAsyncCall', isInAsyncCall)); - } -} diff --git a/lib/main.dart b/lib/main.dart index 3a44cc9..0977885 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -9,6 +9,7 @@ import 'package:intl/date_symbol_data_local.dart'; import 'app.dart'; import 'init.dart'; +import 'settings/preferences_repository.dart'; import 'theme/theme.dart'; import 'tools/tools.dart'; @@ -31,10 +32,11 @@ void main() async { // Logs initLoggy(); - // Prepare theme + // Prepare preferences from SharedPreferences and theme WidgetsFlutterBinding.ensureInitialized(); - final themeRepository = await ThemeRepository.instance; - final themeData = themeRepository.themeData(); + await PreferencesRepository.instance.init(); + final initialThemeData = + PreferencesRepository.instance.value.themePreferences.themeData(); // Manage window on desktop platforms await initializeWindowControl(); @@ -45,11 +47,12 @@ void main() async { await initializeDateFormatting(); // Start up Veilid and Veilid processor in the background - unawaited(initializeVeilid()); + unawaited(initializeVeilidChat()); // Run the app // Hot reloads will only restart this part, not Veilid - runApp(LocalizedApp(delegate, VeilidChatApp(themeData: themeData))); + runApp(LocalizedApp( + delegate, VeilidChatApp(initialThemeData: initialThemeData))); }, (error, stackTrace) { log.error('Dart Runtime: {$error}\n{$stackTrace}'); }); diff --git a/lib/old_to_refactor/entities/entities.dart b/lib/old_to_refactor/entities/entities.dart deleted file mode 100644 index 8a24422..0000000 --- a/lib/old_to_refactor/entities/entities.dart +++ /dev/null @@ -1,3 +0,0 @@ -export 'local_account.dart'; -export 'preferences.dart'; -export 'user_login.dart'; diff --git a/lib/old_to_refactor/providers/account.dart b/lib/old_to_refactor/providers/account.dart deleted file mode 100644 index 3a3caa6..0000000 --- a/lib/old_to_refactor/providers/account.dart +++ /dev/null @@ -1,147 +0,0 @@ -import 'package:freezed_annotation/freezed_annotation.dart'; -import 'package:riverpod_annotation/riverpod_annotation.dart'; - -import '../../entities/local_account.dart'; -import '../../entities/user_login.dart'; -import '../../proto/proto.dart' as proto; -import '../../../packages/veilid_support/veilid_support.dart'; -import '../../local_accounts/local_accounts.dart'; -import 'logins.dart'; - -part 'account.g.dart'; - -enum AccountInfoStatus { - noAccount, - accountInvalid, - accountLocked, - accountReady, -} - -@immutable -class AccountInfo { - const AccountInfo({ - required this.status, - required this.active, - this.account, - }); - - final AccountInfoStatus status; - final bool active; - final proto.Account? account; -} - -/// Get an account from the identity key and if it is logged in and we -/// have its secret available, return the account record contents -@riverpod -Future fetchAccountInfo(FetchAccountInfoRef ref, - {required TypedKey accountMasterRecordKey}) async { - // Get which local account we want to fetch the profile for - final localAccount = await ref.watch( - fetchLocalAccountProvider(accountMasterRecordKey: accountMasterRecordKey) - .future); - if (localAccount == null) { - // Local account does not exist - return const AccountInfo( - status: AccountInfoStatus.noAccount, active: false); - } - - // 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)); - final active = activeUserLogin == accountMasterRecordKey; - - final login = await ref.watch( - fetchLoginProvider(accountMasterRecordKey: accountMasterRecordKey) - .future); - if (login == null) { - // Account was locked - return AccountInfo(status: AccountInfoStatus.accountLocked, active: active); - } - -xxx login should open this key and leave it open, logout should close it - - // Pull the account DHT key, decode it and return it - final pool = await DHTRecordPool.instance(); - final account = await (await pool.openOwned( - login.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 - ref.invalidateSelf(); - return AccountInfo( - status: AccountInfoStatus.accountInvalid, active: active); - } - - // Got account, decrypted and decoded - return AccountInfo( - status: AccountInfoStatus.accountReady, active: active, account: account); -} - -@immutable -class ActiveAccountInfo { - const ActiveAccountInfo({ - required this.localAccount, - required this.userLogin, - required this.account, - }); - // - - KeyPair getConversationWriter() { - final identityKey = localAccount.identityMaster.identityPublicKey; - final identitySecret = userLogin.identitySecret; - return KeyPair(key: identityKey, secret: identitySecret.value); - } - - // - final LocalAccount localAccount; - final UserLogin userLogin; - final proto.Account account; -} - -/// Get the active account info -@riverpod -Future fetchActiveAccountInfo( - FetchActiveAccountInfoRef 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) { - ref.invalidateSelf(); - return null; - } - - // Got account, decrypted and decoded - return ActiveAccountInfo( - localAccount: localAccount, - userLogin: userLogin, - account: account, - ); -} diff --git a/lib/router/cubit/router_cubit.dart b/lib/router/cubit/router_cubit.dart index b182a27..8aec72a 100644 --- a/lib/router/cubit/router_cubit.dart +++ b/lib/router/cubit/router_cubit.dart @@ -7,10 +7,7 @@ import 'package:go_router/go_router.dart'; import '../../../account_manager/account_manager.dart'; import '../../init.dart'; -import '../../old_to_refactor/pages/chat_only.dart'; -import '../../old_to_refactor/pages/home.dart'; -import '../../old_to_refactor/pages/index.dart'; -import '../../old_to_refactor/pages/settings.dart'; +import '../../layout/layout.dart'; import '../../tools/tools.dart'; import '../../veilid_processor/views/developer.dart'; @@ -32,7 +29,7 @@ class RouterCubit extends Cubit { }); // Subscribe to repository streams _accountRepositorySubscription = - accountRepository.changes().listen((event) { + accountRepository.stream().listen((event) { switch (event) { case AccountRepositoryChange.localAccounts: emit(state.copyWith( @@ -98,8 +95,8 @@ class RouterCubit extends Cubit { switch (goRouterState.matchedLocation) { case '/': - // Wait for veilid to be initialized - if (!eventualVeilid.isCompleted) { + // Wait for initialization to complete + if (!eventualInitialized.isCompleted) { return null; } diff --git a/lib/settings/models/models.dart b/lib/settings/models/models.dart new file mode 100644 index 0000000..b7a1cbf --- /dev/null +++ b/lib/settings/models/models.dart @@ -0,0 +1 @@ +export 'preferences.dart'; diff --git a/lib/old_to_refactor/entities/preferences.dart b/lib/settings/models/preferences.dart similarity index 75% rename from lib/old_to_refactor/entities/preferences.dart rename to lib/settings/models/preferences.dart index f0e3e82..8ff626f 100644 --- a/lib/old_to_refactor/entities/preferences.dart +++ b/lib/settings/models/preferences.dart @@ -1,6 +1,8 @@ import 'package:change_case/change_case.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; +import '../../theme/theme.dart'; + part 'preferences.freezed.dart'; part 'preferences.g.dart'; @@ -16,6 +18,12 @@ class LockPreference with _$LockPreference { factory LockPreference.fromJson(dynamic json) => _$LockPreferenceFromJson(json as Map); + + static const LockPreference defaults = LockPreference( + inactivityLockSecs: 0, + lockWhenSwitching: false, + lockWithSystemLock: false, + ); } // Theme supports multiple translations @@ -25,6 +33,8 @@ enum LanguagePreference { factory LanguagePreference.fromJson(dynamic j) => LanguagePreference.values.byName((j as String).toCamelCase()); String toJson() => name.toPascalCase(); + + static const LanguagePreference defaults = LanguagePreference.englishUS; } // Preferences are stored in a table locally and globally affect all @@ -39,4 +49,9 @@ class Preferences with _$Preferences { factory Preferences.fromJson(dynamic json) => _$PreferencesFromJson(json as Map); + + static const Preferences defaults = Preferences( + themePreferences: ThemePreferences.defaults, + language: LanguagePreference.defaults, + locking: LockPreference.defaults); } diff --git a/lib/old_to_refactor/entities/preferences.freezed.dart b/lib/settings/models/preferences.freezed.dart similarity index 94% rename from lib/old_to_refactor/entities/preferences.freezed.dart rename to lib/settings/models/preferences.freezed.dart index 09484e2..3a3e6c7 100644 --- a/lib/old_to_refactor/entities/preferences.freezed.dart +++ b/lib/settings/models/preferences.freezed.dart @@ -226,6 +226,7 @@ abstract class $PreferencesCopyWith<$Res> { LanguagePreference language, LockPreference locking}); + $ThemePreferencesCopyWith<$Res> get themePreferences; $LockPreferenceCopyWith<$Res> get locking; } @@ -242,12 +243,12 @@ class _$PreferencesCopyWithImpl<$Res, $Val extends Preferences> @pragma('vm:prefer-inline') @override $Res call({ - Object? themePreferences = freezed, + Object? themePreferences = null, Object? language = null, Object? locking = null, }) { return _then(_value.copyWith( - themePreferences: freezed == themePreferences + themePreferences: null == themePreferences ? _value.themePreferences : themePreferences // ignore: cast_nullable_to_non_nullable as ThemePreferences, @@ -262,6 +263,14 @@ class _$PreferencesCopyWithImpl<$Res, $Val extends Preferences> ) as $Val); } + @override + @pragma('vm:prefer-inline') + $ThemePreferencesCopyWith<$Res> get themePreferences { + return $ThemePreferencesCopyWith<$Res>(_value.themePreferences, (value) { + return _then(_value.copyWith(themePreferences: value) as $Val); + }); + } + @override @pragma('vm:prefer-inline') $LockPreferenceCopyWith<$Res> get locking { @@ -284,6 +293,8 @@ abstract class _$$PreferencesImplCopyWith<$Res> LanguagePreference language, LockPreference locking}); + @override + $ThemePreferencesCopyWith<$Res> get themePreferences; @override $LockPreferenceCopyWith<$Res> get locking; } @@ -299,12 +310,12 @@ class __$$PreferencesImplCopyWithImpl<$Res> @pragma('vm:prefer-inline') @override $Res call({ - Object? themePreferences = freezed, + Object? themePreferences = null, Object? language = null, Object? locking = null, }) { return _then(_$PreferencesImpl( - themePreferences: freezed == themePreferences + themePreferences: null == themePreferences ? _value.themePreferences : themePreferences // ignore: cast_nullable_to_non_nullable as ThemePreferences, @@ -348,8 +359,8 @@ class _$PreferencesImpl implements _Preferences { return identical(this, other) || (other.runtimeType == runtimeType && other is _$PreferencesImpl && - const DeepCollectionEquality() - .equals(other.themePreferences, themePreferences) && + (identical(other.themePreferences, themePreferences) || + other.themePreferences == themePreferences) && (identical(other.language, language) || other.language == language) && (identical(other.locking, locking) || other.locking == locking)); @@ -357,8 +368,8 @@ class _$PreferencesImpl implements _Preferences { @JsonKey(ignore: true) @override - int get hashCode => Object.hash(runtimeType, - const DeepCollectionEquality().hash(themePreferences), language, locking); + int get hashCode => + Object.hash(runtimeType, themePreferences, language, locking); @JsonKey(ignore: true) @override diff --git a/lib/settings/models/preferences.g.dart b/lib/settings/models/preferences.g.dart new file mode 100644 index 0000000..af010d6 --- /dev/null +++ b/lib/settings/models/preferences.g.dart @@ -0,0 +1,36 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'preferences.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +_$LockPreferenceImpl _$$LockPreferenceImplFromJson(Map json) => + _$LockPreferenceImpl( + inactivityLockSecs: json['inactivity_lock_secs'] as int, + lockWhenSwitching: json['lock_when_switching'] as bool, + lockWithSystemLock: json['lock_with_system_lock'] as bool, + ); + +Map _$$LockPreferenceImplToJson( + _$LockPreferenceImpl instance) => + { + 'inactivity_lock_secs': instance.inactivityLockSecs, + 'lock_when_switching': instance.lockWhenSwitching, + 'lock_with_system_lock': instance.lockWithSystemLock, + }; + +_$PreferencesImpl _$$PreferencesImplFromJson(Map json) => + _$PreferencesImpl( + themePreferences: ThemePreferences.fromJson(json['theme_preferences']), + language: LanguagePreference.fromJson(json['language']), + locking: LockPreference.fromJson(json['locking']), + ); + +Map _$$PreferencesImplToJson(_$PreferencesImpl instance) => + { + 'theme_preferences': instance.themePreferences.toJson(), + 'language': instance.language.toJson(), + 'locking': instance.locking.toJson(), + }; diff --git a/lib/settings/preferences_cubit.dart b/lib/settings/preferences_cubit.dart new file mode 100644 index 0000000..e9f69b5 --- /dev/null +++ b/lib/settings/preferences_cubit.dart @@ -0,0 +1,9 @@ +import '../tools/tools.dart'; +import 'settings.dart'; + +xxx convert to non-asyncvalue based wrapper since there's always a default here + +class PreferencesCubit extends StreamWrapperCubit { + PreferencesCubit(PreferencesRepository repository) + : super(repository.stream, defaultState: repository.value); +} diff --git a/lib/settings/preferences_repository.dart b/lib/settings/preferences_repository.dart new file mode 100644 index 0000000..d7b8f48 --- /dev/null +++ b/lib/settings/preferences_repository.dart @@ -0,0 +1,34 @@ +import 'dart:async'; + +import 'package:shared_preferences/shared_preferences.dart'; + +import '../tools/tools.dart'; +import 'models/models.dart'; + +class PreferencesRepository { + PreferencesRepository._(); + + late final SharedPreferencesValue _data; + + Preferences get value => _data.requireValue; + Stream get stream => _data.stream; + + ////////////////////////////////////////////////////////////// + /// Singleton initialization + + static PreferencesRepository instance = PreferencesRepository._(); + + Future init() async { + final sharedPreferences = await SharedPreferences.getInstance(); + _data = SharedPreferencesValue( + sharedPreferences: sharedPreferences, + keyName: 'preferences', + valueFromJson: (obj) => + obj != null ? Preferences.fromJson(obj) : Preferences.defaults, + valueToJson: (val) => val.toJson()); + await _data.get(); + } + + Future set(Preferences value) => _data.set(value); + Future get() => _data.get(); +} diff --git a/lib/settings/settings.dart b/lib/settings/settings.dart new file mode 100644 index 0000000..b56c1a4 --- /dev/null +++ b/lib/settings/settings.dart @@ -0,0 +1,4 @@ +export 'models/models.dart'; +export 'preferences_cubit.dart'; +export 'preferences_repository.dart'; +export 'settings_page.dart'; diff --git a/lib/settings/settings_page.dart b/lib/settings/settings_page.dart new file mode 100644 index 0000000..e13fd3c --- /dev/null +++ b/lib/settings/settings_page.dart @@ -0,0 +1,129 @@ +import 'package:animated_theme_switcher/animated_theme_switcher.dart'; +import 'package:awesome_extensions/awesome_extensions.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_form_builder/flutter_form_builder.dart'; +import 'package:flutter_translate/flutter_translate.dart'; +import 'package:veilid_support/veilid_support.dart'; + +import '../layout/default_app_bar.dart'; +import '../theme/theme.dart'; +import '../veilid_processor/veilid_processor.dart'; +import 'preferences_cubit.dart'; +import 'preferences_repository.dart'; +import 'settings.dart'; + +class SettingsPage extends StatefulWidget { + const SettingsPage({super.key}); + + @override + SettingsPageState createState() => SettingsPageState(); +} + +class SettingsPageState extends State { + final _formKey = GlobalKey(); + static const String formFieldTheme = 'theme'; + static const String formFieldBrightness = 'brightness'; + + @override + void initState() { + super.initState(); + } + + List> _getThemeDropdownItems() { + const colorPrefs = ColorPreference.values; + final colorNames = { + ColorPreference.scarlet: translate('themes.scarlet'), + ColorPreference.vapor: translate('themes.vapor'), + ColorPreference.babydoll: translate('themes.babydoll'), + ColorPreference.gold: translate('themes.gold'), + ColorPreference.garden: translate('themes.garden'), + ColorPreference.forest: translate('themes.forest'), + ColorPreference.arctic: translate('themes.arctic'), + ColorPreference.lapis: translate('themes.lapis'), + ColorPreference.eggplant: translate('themes.eggplant'), + ColorPreference.lime: translate('themes.lime'), + ColorPreference.grim: translate('themes.grim'), + ColorPreference.contrast: translate('themes.contrast') + }; + + return colorPrefs + .map((e) => DropdownMenuItem(value: e, child: Text(colorNames[e]!))) + .toList(); + } + + List> _getBrightnessDropdownItems() { + const brightnessPrefs = BrightnessPreference.values; + final brightnessNames = { + BrightnessPreference.system: translate('brightness.system'), + BrightnessPreference.light: translate('brightness.light'), + BrightnessPreference.dark: translate('brightness.dark') + }; + + return brightnessPrefs + .map( + (e) => DropdownMenuItem(value: e, child: Text(brightnessNames[e]!))) + .toList(); + } + + @override + Widget build(BuildContext context) => BlocBuilder>( + builder: (context, state) => ThemeSwitchingArea( + child: Scaffold( + // resizeToAvoidBottomInset: false, + appBar: DefaultAppBar( + title: Text(translate('settings_page.titlebar')), + leading: IconButton( + icon: const Icon(Icons.arrow_back), + onPressed: () => context.pop(), + ), + actions: [ + const SignalStrengthMeterWidget().paddingLTRB(16, 0, 16, 0), + ]), + + body: FormBuilder( + key: _formKey, + child: ListView( + children: [ + ThemeSwitcher.withTheme( + builder: (_, switcher, theme) => FormBuilderDropdown( + name: formFieldTheme, + decoration: InputDecoration( + label: + Text(translate('settings_page.color_theme'))), + items: _getThemeDropdownItems(), + initialValue: themePreferences.colorPreference, + onChanged: (value) async { + final newPrefs = themePreferences.copyWith( + colorPreference: value as ColorPreference); + await themeService.save(newPrefs); + switcher.changeTheme( + theme: themeService.get(newPrefs)); + ref.invalidate(themeServiceProvider); + setState(() {}); + })), + ThemeSwitcher.withTheme( + builder: (_, switcher, theme) => FormBuilderDropdown( + name: formFieldBrightness, + decoration: InputDecoration( + label: Text( + translate('settings_page.brightness_mode'))), + items: _getBrightnessDropdownItems(), + initialValue: themePreferences.brightnessPreference, + onChanged: (value) async { + final newPrefs = themePreferences.copyWith( + brightnessPreference: + value as BrightnessPreference); + await themeService.save(newPrefs); + switcher.changeTheme( + theme: themeService.get(newPrefs)); + ref.invalidate(themeServiceProvider); + setState(() {}); + })), + ], + ), + ).paddingSymmetric(horizontal: 24, vertical: 8), + ))); +} diff --git a/lib/theme/models/models.dart b/lib/theme/models/models.dart index e5e69d9..c22e8ab 100644 --- a/lib/theme/models/models.dart +++ b/lib/theme/models/models.dart @@ -1,4 +1,4 @@ +export 'radix_generator.dart'; export 'scale_color.dart'; export 'scale_scheme.dart'; export 'theme_preference.dart'; -export 'radix_generator.dart'; diff --git a/lib/theme/models/theme_preference.dart b/lib/theme/models/theme_preference.dart index 0ac25e5..74c90d8 100644 --- a/lib/theme/models/theme_preference.dart +++ b/lib/theme/models/theme_preference.dart @@ -1,6 +1,10 @@ import 'package:change_case/change_case.dart'; +import 'package:flutter/material.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; +import '../../tools/tools.dart'; +import 'radix_generator.dart'; + part 'theme_preference.freezed.dart'; part 'theme_preference.g.dart'; @@ -49,4 +53,62 @@ class ThemePreferences with _$ThemePreferences { factory ThemePreferences.fromJson(dynamic json) => _$ThemePreferencesFromJson(json as Map); + + static const ThemePreferences defaults = ThemePreferences( + colorPreference: ColorPreference.vapor, + brightnessPreference: BrightnessPreference.system, + displayScale: 1, + ); +} + +extension ThemePreferencesExt on ThemePreferences { + /// Get material 'ThemeData' for existinb + ThemeData themeData() { + late final Brightness brightness; + switch (brightnessPreference) { + case BrightnessPreference.system: + if (isPlatformDark) { + brightness = Brightness.dark; + } else { + brightness = Brightness.light; + } + case BrightnessPreference.light: + brightness = Brightness.light; + case BrightnessPreference.dark: + brightness = Brightness.dark; + } + + late final ThemeData themeData; + switch (colorPreference) { + // Special cases + case ColorPreference.contrast: + // xxx do contrastGenerator + themeData = radixGenerator(brightness, RadixThemeColor.grim); + // Generate from Radix + case ColorPreference.scarlet: + themeData = radixGenerator(brightness, RadixThemeColor.scarlet); + case ColorPreference.babydoll: + themeData = radixGenerator(brightness, RadixThemeColor.babydoll); + case ColorPreference.vapor: + themeData = radixGenerator(brightness, RadixThemeColor.vapor); + case ColorPreference.gold: + themeData = radixGenerator(brightness, RadixThemeColor.gold); + case ColorPreference.garden: + themeData = radixGenerator(brightness, RadixThemeColor.garden); + case ColorPreference.forest: + themeData = radixGenerator(brightness, RadixThemeColor.forest); + case ColorPreference.arctic: + themeData = radixGenerator(brightness, RadixThemeColor.arctic); + case ColorPreference.lapis: + themeData = radixGenerator(brightness, RadixThemeColor.lapis); + case ColorPreference.eggplant: + themeData = radixGenerator(brightness, RadixThemeColor.eggplant); + case ColorPreference.lime: + themeData = radixGenerator(brightness, RadixThemeColor.lime); + case ColorPreference.grim: + themeData = radixGenerator(brightness, RadixThemeColor.grim); + } + + return themeData; + } } diff --git a/lib/theme/repository/theme_repository.dart b/lib/theme/repository/theme_repository.dart deleted file mode 100644 index 2aae8cd..0000000 --- a/lib/theme/repository/theme_repository.dart +++ /dev/null @@ -1,131 +0,0 @@ -// ignore_for_file: always_put_required_named_parameters_first - -import 'dart:convert'; - -import 'package:flutter/material.dart'; -import 'package:shared_preferences/shared_preferences.dart'; - -import '../models/models.dart'; - -//////////////////////////////////////////////////////////////////////// - -class ThemeRepository { - ThemeRepository._({required SharedPreferences sharedPreferences}) - : _sharedPreferences = sharedPreferences, - _themePreferences = defaultThemePreferences; - - final SharedPreferences _sharedPreferences; - ThemePreferences _themePreferences; - ThemeData? _cachedThemeData; - - /// Singleton instance of ThemeRepository - static ThemeRepository? _instance; - static Future get instance async { - if (_instance == null) { - final sharedPreferences = await SharedPreferences.getInstance(); - final instance = ThemeRepository._(sharedPreferences: sharedPreferences); - await instance.load(); - _instance = instance; - } - return _instance!; - } - - static bool get isPlatformDark => - WidgetsBinding.instance.platformDispatcher.platformBrightness == - Brightness.dark; - - /// Defaults - static ThemePreferences get defaultThemePreferences => const ThemePreferences( - colorPreference: ColorPreference.vapor, - brightnessPreference: BrightnessPreference.system, - displayScale: 1, - ); - - /// Get theme preferences - ThemePreferences get themePreferences => _themePreferences; - - /// Set theme preferences - void setThemePreferences(ThemePreferences themePreferences) { - _themePreferences = themePreferences; - _cachedThemeData = null; - } - - /// Load theme preferences from storage - Future load() async { - final themePreferencesJson = - _sharedPreferences.getString('themePreferences'); - - ThemePreferences? newThemePreferences; - if (themePreferencesJson != null) { - try { - newThemePreferences = - ThemePreferences.fromJson(jsonDecode(themePreferencesJson)); - // ignore: avoid_catches_without_on_clauses - } catch (_) { - // ignore - } - } - setThemePreferences(newThemePreferences ?? defaultThemePreferences); - } - - /// Save theme preferences to storage - Future save() async { - await _sharedPreferences.setString( - 'themePreferences', jsonEncode(_themePreferences.toJson())); - } - - /// Get material 'ThemeData' for existinb - ThemeData themeData() { - final cachedThemeData = _cachedThemeData; - if (cachedThemeData != null) { - return cachedThemeData; - } - late final Brightness brightness; - switch (_themePreferences.brightnessPreference) { - case BrightnessPreference.system: - if (isPlatformDark) { - brightness = Brightness.dark; - } else { - brightness = Brightness.light; - } - case BrightnessPreference.light: - brightness = Brightness.light; - case BrightnessPreference.dark: - brightness = Brightness.dark; - } - - late final ThemeData themeData; - switch (_themePreferences.colorPreference) { - // Special cases - case ColorPreference.contrast: - // xxx do contrastGenerator - themeData = radixGenerator(brightness, RadixThemeColor.grim); - // Generate from Radix - case ColorPreference.scarlet: - themeData = radixGenerator(brightness, RadixThemeColor.scarlet); - case ColorPreference.babydoll: - themeData = radixGenerator(brightness, RadixThemeColor.babydoll); - case ColorPreference.vapor: - themeData = radixGenerator(brightness, RadixThemeColor.vapor); - case ColorPreference.gold: - themeData = radixGenerator(brightness, RadixThemeColor.gold); - case ColorPreference.garden: - themeData = radixGenerator(brightness, RadixThemeColor.garden); - case ColorPreference.forest: - themeData = radixGenerator(brightness, RadixThemeColor.forest); - case ColorPreference.arctic: - themeData = radixGenerator(brightness, RadixThemeColor.arctic); - case ColorPreference.lapis: - themeData = radixGenerator(brightness, RadixThemeColor.lapis); - case ColorPreference.eggplant: - themeData = radixGenerator(brightness, RadixThemeColor.eggplant); - case ColorPreference.lime: - themeData = radixGenerator(brightness, RadixThemeColor.lime); - case ColorPreference.grim: - themeData = radixGenerator(brightness, RadixThemeColor.grim); - } - - _cachedThemeData = themeData; - return themeData; - } -} diff --git a/lib/theme/theme.dart b/lib/theme/theme.dart index 21be85e..51b0215 100644 --- a/lib/theme/theme.dart +++ b/lib/theme/theme.dart @@ -1,2 +1 @@ export 'models/models.dart'; -export 'repository/theme_repository.dart'; diff --git a/lib/tools/shared_preferences.dart b/lib/tools/shared_preferences.dart new file mode 100644 index 0000000..ce1838d --- /dev/null +++ b/lib/tools/shared_preferences.dart @@ -0,0 +1,80 @@ +import 'dart:async'; +import 'dart:convert'; + +import 'package:shared_preferences/shared_preferences.dart'; + +abstract mixin class SharedPreferencesBacked { + SharedPreferences get _sharedPreferences; + String keyName(); + T valueFromJson(Object? obj); + Object? valueToJson(T val); + + /// Load things from storage + Future load() async { + final valueJsonStr = _sharedPreferences.getString(keyName()); + final Object? valueJsonObj = + valueJsonStr != null ? jsonDecode(valueJsonStr) : null; + return valueFromJson(valueJsonObj); + } + + /// Store things to storage + Future store(T obj) async { + final valueJsonObj = valueToJson(obj); + if (valueJsonObj == null) { + await _sharedPreferences.remove(keyName()); + } else { + await _sharedPreferences.setString(keyName(), jsonEncode(valueJsonObj)); + } + return obj; + } +} + +class SharedPreferencesValue extends SharedPreferencesBacked { + SharedPreferencesValue({ + required SharedPreferences sharedPreferences, + required String keyName, + required T Function(Object? obj) valueFromJson, + required Object? Function(T obj) valueToJson, + }) : _sharedPreferencesInstance = sharedPreferences, + _valueFromJson = valueFromJson, + _valueToJson = valueToJson, + _keyName = keyName, + _streamController = StreamController.broadcast(); + + @override + SharedPreferences get _sharedPreferences => _sharedPreferencesInstance; + + T? get value => _value; + T get requireValue => _value!; + Stream get stream => _streamController.stream; + + Future get() async { + final val = _value; + if (val != null) { + return val; + } + final loadedValue = await load(); + return _value = loadedValue; + } + + Future set(T newVal) async { + _value = await store(newVal); + _streamController.add(newVal); + } + + T? _value; + final SharedPreferences _sharedPreferencesInstance; + final String _keyName; + final T Function(Object? obj) _valueFromJson; + final Object? Function(T obj) _valueToJson; + final StreamController _streamController; + + ////////////////////////////////////////////////////////////// + /// SharedPreferencesBacked + @override + String keyName() => _keyName; + @override + T valueFromJson(Object? obj) => _valueFromJson(obj); + @override + Object? valueToJson(T val) => _valueToJson(val); +} diff --git a/lib/tools/tools.dart b/lib/tools/tools.dart index 147829f..5532a56 100644 --- a/lib/tools/tools.dart +++ b/lib/tools/tools.dart @@ -3,6 +3,7 @@ export 'loggy.dart'; export 'phono_byte.dart'; export 'responsive.dart'; export 'scanner_error_widget.dart'; +export 'shared_preferences.dart'; export 'state_logger.dart'; export 'stream_wrapper_cubit.dart'; export 'widget_helpers.dart'; diff --git a/lib/tools/widget_helpers.dart b/lib/tools/widget_helpers.dart index 618f377..90fb9bf 100644 --- a/lib/tools/widget_helpers.dart +++ b/lib/tools/widget_helpers.dart @@ -5,6 +5,7 @@ import 'package:flutter_spinkit/flutter_spinkit.dart'; import 'package:flutter_translate/flutter_translate.dart'; import 'package:motion_toast/motion_toast.dart'; import 'package:quickalert/quickalert.dart'; +import 'package:veilid_support/veilid_support.dart'; import '../theme/theme.dart'; @@ -41,6 +42,24 @@ Widget waitingPage(BuildContext context) => ColoredBox( color: Theme.of(context).scaffoldBackgroundColor, child: Center(child: buildProgressIndicator(context))); +Widget errorPage(BuildContext context, Object err, StackTrace? st) => + ColoredBox( + color: Theme.of(context).colorScheme.error, + child: Center(child: Text(err.toString()))); + +Widget asyncValueBuilder( + AsyncValue av, Widget Function(BuildContext, T) builder) => + av.when( + loading: () => const Builder(builder: waitingPage), + error: (e, st) => + Builder(builder: (context) => errorPage(context, e, st)), + data: (d) => Builder(builder: (context) => builder(context, d))); + +extension AsyncValueBuilderExt on AsyncValue { + Widget builder(Widget Function(BuildContext, T) builder) => + asyncValueBuilder(this, builder); +} + Future showErrorModal( BuildContext context, String title, String text) async { await QuickAlert.show( @@ -135,3 +154,7 @@ Future showStyledDialog( borderRadius: BorderRadius.circular(12))), child: child.paddingAll(0))))); } + +bool get isPlatformDark => + WidgetsBinding.instance.platformDispatcher.platformBrightness == + Brightness.dark; diff --git a/lib/tools/window_control.dart b/lib/tools/window_control.dart index d3be316..c6e33d3 100644 --- a/lib/tools/window_control.dart +++ b/lib/tools/window_control.dart @@ -27,14 +27,15 @@ Future initializeWindowControl() async { skipTaskbar: false, ); await windowManager.waitUntilReadyToShow(windowOptions, () async { - await _doWindowSetup(TitleBarStyle.hidden, OrientationCapability.normal); + await changeWindowSetup( + TitleBarStyle.hidden, OrientationCapability.normal); await windowManager.show(); await windowManager.focus(); }); } } -Future _doWindowSetup(TitleBarStyle titleBarStyle, +Future changeWindowSetup(TitleBarStyle titleBarStyle, OrientationCapability orientationCapability) async { if (isDesktop) { await windowManager.setTitleBarStyle(titleBarStyle); @@ -58,8 +59,3 @@ Future _doWindowSetup(TitleBarStyle titleBarStyle, } } } - -Future changeWindowSetup(TitleBarStyle titleBarStyle, - OrientationCapability orientationCapability) async { - await _doWindowSetup(titleBarStyle, orientationCapability); -} diff --git a/packages/veilid_support/lib/dht_support/src/dht_record.dart b/packages/veilid_support/lib/dht_support/src/dht_record.dart index 7c7b167..1d994b3 100644 --- a/packages/veilid_support/lib/dht_support/src/dht_record.dart +++ b/packages/veilid_support/lib/dht_support/src/dht_record.dart @@ -262,7 +262,6 @@ class DHTRecord { eventualUpdateBytes(protobufUpdate(fromBuffer, update), subkey: subkey); Future watch( - Future Function(VeilidUpdateValueChange update) onUpdate, {List? subkeys, Timestamp? expiration, int? count}) async { diff --git a/packages/veilid_support/lib/dht_support/src/dht_record_cubit.dart b/packages/veilid_support/lib/dht_support/src/dht_record_cubit.dart index bd233bb..00db3fd 100644 --- a/packages/veilid_support/lib/dht_support/src/dht_record_cubit.dart +++ b/packages/veilid_support/lib/dht_support/src/dht_record_cubit.dart @@ -5,8 +5,8 @@ import 'package:bloc/bloc.dart'; import '../../veilid_support.dart'; -class DhtRecordCubit extends Cubit> { - DhtRecordCubit({ +class DHTRecordCubit extends Cubit> { + DHTRecordCubit({ required DHTRecord record, required Future Function(DHTRecord) initialStateFunction, required Future Function(DHTRecord, List, ValueData) @@ -48,7 +48,7 @@ class DhtRecordCubit extends Cubit> { } // Cubit that watches the default subkey value of a dhtrecord -class DefaultDHTRecordCubit extends DhtRecordCubit { +class DefaultDHTRecordCubit extends DHTRecordCubit { DefaultDHTRecordCubit({ required super.record, required T Function(List data) decodeState, diff --git a/packages/veilid_support/lib/src/table_db.dart b/packages/veilid_support/lib/src/table_db.dart index 756c7db..f6a69b7 100644 --- a/packages/veilid_support/lib/src/table_db.dart +++ b/packages/veilid_support/lib/src/table_db.dart @@ -64,10 +64,12 @@ class TableDBValue extends TableDBBacked { }) : _tableName = tableName, _valueFromJson = valueFromJson, _valueToJson = valueToJson, - _tableKeyName = tableKeyName; + _tableKeyName = tableKeyName, + _streamController = StreamController.broadcast(); T? get value => _value; T get requireValue => _value!; + Stream get stream => _streamController.stream; Future get() async { final val = _value; @@ -80,6 +82,7 @@ class TableDBValue extends TableDBBacked { Future set(T newVal) async { _value = await store(newVal); + _streamController.add(newVal); } T? _value; @@ -87,6 +90,7 @@ class TableDBValue extends TableDBBacked { final String _tableKeyName; final T Function(Object? obj) _valueFromJson; final Object? Function(T obj) _valueToJson; + final StreamController _streamController; ////////////////////////////////////////////////////////////// /// AsyncTableDBBacked From c7b541c643272ef94d03f19b7ea5bd7b46ecbd90 Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Tue, 9 Jan 2024 20:58:27 -0500 Subject: [PATCH 09/68] more refactor --- lib/account_manager/account_manager.dart | 1 + lib/chat/chat.dart | 1 + lib/chat/views/build_chat_component.dart | 37 ++ .../views}/chat_component.dart | 14 +- lib/chat/views/empty_chat_widget.dart | 34 ++ lib/chat/views/no_conversation_widget.dart | 35 ++ lib/chat/views/views.dart | 1 + .../chat_list.dart} | 0 .../chat_single_contact_item_widget.dart | 0 .../chat_single_contact_list_widget.dart | 0 .../views}/empty_chat_list_widget.dart | 7 +- lib/chat_list/views/views.dart | 0 .../contact_invitation.dart | 1 + .../paste_invite_dialog.dart | 6 +- .../scan_invite_dialog.dart | 4 +- .../views}/contact_invitation_display.dart | 9 +- .../contact_invitation_item_widget.dart | 11 +- .../contact_invitation_list_widget.dart | 9 +- .../views}/invite_dialog.dart | 15 +- .../new_contact_invitation_bottom_sheet.dart | 65 +++ .../views/paste_invite_dialog.dart | 131 ++++++ .../views/scan_invite_dialog.dart | 395 ++++++++++++++++++ .../views}/send_invite_dialog.dart | 18 +- lib/contact_invitation/views/views.dart | 8 + lib/contacts/contacts.dart | 1 + .../views}/contact_item_widget.dart | 15 +- .../views}/contact_list_widget.dart | 0 .../views}/empty_contact_list_widget.dart | 9 +- lib/contacts/views/views.dart | 3 + lib/layout/chat_only.dart | 23 +- lib/layout/edit_contact.dart | 42 +- lib/layout/home.dart | 36 +- lib/layout/main_pager/account.dart | 17 +- .../bottom_sheet_action_button.dart | 6 +- lib/layout/main_pager/chats.dart | 17 +- lib/layout/main_pager/main_pager.dart | 77 +--- .../components/empty_chat_widget.dart | 36 -- .../components/no_conversation_widget.dart | 35 -- .../managers/valid_contact_invitation.dart | 1 - lib/settings/preferences_cubit.dart | 2 - lib/settings/settings_page.dart | 40 +- .../components => tools}/enter_password.dart | 7 +- .../components => tools}/enter_pin.dart | 7 +- lib/tools/tools.dart | 3 + lib/tools/widget_helpers.dart | 17 + 45 files changed, 860 insertions(+), 336 deletions(-) create mode 100644 lib/chat/chat.dart create mode 100644 lib/chat/views/build_chat_component.dart rename lib/{old_to_refactor/components => chat/views}/chat_component.dart (94%) create mode 100644 lib/chat/views/empty_chat_widget.dart create mode 100644 lib/chat/views/no_conversation_widget.dart create mode 100644 lib/chat/views/views.dart rename lib/{old_to_refactor/managers/contact_list_manager.dart => chat_list/chat_list.dart} (100%) rename lib/{old_to_refactor/components => chat_list/views}/chat_single_contact_item_widget.dart (100%) rename lib/{old_to_refactor/components => chat_list/views}/chat_single_contact_list_widget.dart (100%) rename lib/{old_to_refactor/components => chat_list/views}/empty_chat_list_widget.dart (79%) create mode 100644 lib/chat_list/views/views.dart create mode 100644 lib/contact_invitation/contact_invitation.dart rename lib/{old_to_refactor/components => contact_invitation}/paste_invite_dialog.dart (96%) rename lib/{old_to_refactor/components => contact_invitation}/scan_invite_dialog.dart (99%) rename lib/{old_to_refactor/components => contact_invitation/views}/contact_invitation_display.dart (95%) rename lib/{old_to_refactor/components => contact_invitation/views}/contact_invitation_item_widget.dart (94%) rename lib/{old_to_refactor/components => contact_invitation/views}/contact_invitation_list_widget.dart (91%) rename lib/{old_to_refactor/components => contact_invitation/views}/invite_dialog.dart (96%) create mode 100644 lib/contact_invitation/views/new_contact_invitation_bottom_sheet.dart create mode 100644 lib/contact_invitation/views/paste_invite_dialog.dart create mode 100644 lib/contact_invitation/views/scan_invite_dialog.dart rename lib/{old_to_refactor/components => contact_invitation/views}/send_invite_dialog.dart (93%) create mode 100644 lib/contact_invitation/views/views.dart create mode 100644 lib/contacts/contacts.dart rename lib/{old_to_refactor/components => contacts/views}/contact_item_widget.dart (92%) rename lib/{old_to_refactor/components => contacts/views}/contact_list_widget.dart (100%) rename lib/{old_to_refactor/components => contacts/views}/empty_contact_list_widget.dart (80%) create mode 100644 lib/contacts/views/views.dart rename lib/{old_to_refactor/components => layout/main_pager}/bottom_sheet_action_button.dart (90%) delete mode 100644 lib/old_to_refactor/components/empty_chat_widget.dart delete mode 100644 lib/old_to_refactor/components/no_conversation_widget.dart delete mode 100644 lib/old_to_refactor/managers/valid_contact_invitation.dart rename lib/{old_to_refactor/components => tools}/enter_password.dart (94%) rename lib/{old_to_refactor/components => tools}/enter_pin.dart (95%) diff --git a/lib/account_manager/account_manager.dart b/lib/account_manager/account_manager.dart index f4b3f22..af728ac 100644 --- a/lib/account_manager/account_manager.dart +++ b/lib/account_manager/account_manager.dart @@ -1,3 +1,4 @@ export 'cubit/cubit.dart'; +export 'models/models.dart'; export 'repository/repository.dart'; export 'views/views.dart'; diff --git a/lib/chat/chat.dart b/lib/chat/chat.dart new file mode 100644 index 0000000..83d1303 --- /dev/null +++ b/lib/chat/chat.dart @@ -0,0 +1 @@ +export 'views/views.dart'; diff --git a/lib/chat/views/build_chat_component.dart b/lib/chat/views/build_chat_component.dart new file mode 100644 index 0000000..aa74ff2 --- /dev/null +++ b/lib/chat/views/build_chat_component.dart @@ -0,0 +1,37 @@ +import 'package:flutter/material.dart'; + +import '../../tools/tools.dart'; + +Widget buildChatComponent() { + // final contactList = ref.watch(fetchContactListProvider).asData?.value ?? + // const IListConst([]); + + // final activeChat = ref.watch(activeChatStateProvider); + // if (activeChat == null) { + // return const EmptyChatWidget(); + // } + + // final activeAccountInfo = + // ref.watch(fetchActiveAccountProvider).asData?.value; + // if (activeAccountInfo == null) { + // return const EmptyChatWidget(); + // } + + // final activeChatContactIdx = contactList.indexWhere( + // (c) => + // proto.TypedKeyProto.fromProto(c.remoteConversationRecordKey) == + // activeChat, + // ); + // if (activeChatContactIdx == -1) { + // ref.read(activeChatStateProvider.notifier).state = null; + // return const EmptyChatWidget(); + // } + // final activeChatContact = contactList[activeChatContactIdx]; + + // return ChatComponent( + // activeAccountInfo: activeAccountInfo, + // activeChat: activeChat, + // activeChatContact: activeChatContact); + // } + return Builder(builder: waitingPage); +} diff --git a/lib/old_to_refactor/components/chat_component.dart b/lib/chat/views/chat_component.dart similarity index 94% rename from lib/old_to_refactor/components/chat_component.dart rename to lib/chat/views/chat_component.dart index ff1b642..154f148 100644 --- a/lib/old_to_refactor/components/chat_component.dart +++ b/lib/chat/views/chat_component.dart @@ -7,13 +7,13 @@ import 'package:flutter_chat_types/flutter_chat_types.dart' as types; import 'package:flutter_chat_ui/flutter_chat_ui.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import '../proto/proto.dart' as proto; -import '../providers/account.dart'; -import '../providers/chat.dart'; -import '../providers/conversation.dart'; -import '../tools/tools.dart'; -import '../veilid_init.dart'; -import '../veilid_support/veilid_support.dart'; +import '../../old_to_refactor/proto/proto.dart' as proto; +import '../../old_to_refactor/providers/account.dart'; +import '../../old_to_refactor/providers/chat.dart'; +import '../../old_to_refactor/providers/conversation.dart'; +import '../../old_to_refactor/tools/tools.dart'; +import '../../old_to_refactor/veilid_init.dart'; +import '../../old_to_refactor/veilid_support/veilid_support.dart'; class ChatComponent extends ConsumerStatefulWidget { const ChatComponent( diff --git a/lib/chat/views/empty_chat_widget.dart b/lib/chat/views/empty_chat_widget.dart new file mode 100644 index 0000000..a9072cd --- /dev/null +++ b/lib/chat/views/empty_chat_widget.dart @@ -0,0 +1,34 @@ +import 'package:flutter/material.dart'; + +class EmptyChatWidget extends StatelessWidget { + const EmptyChatWidget({super.key}); + + @override + // ignore: prefer_expression_function_bodies + Widget build( + BuildContext context, + ) => + Container( + width: double.infinity, + height: double.infinity, + decoration: BoxDecoration( + color: Theme.of(context).scaffoldBackgroundColor, + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.chat, + color: Theme.of(context).disabledColor, + size: 48, + ), + Text( + 'Say Something', + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: Theme.of(context).disabledColor, + ), + ), + ], + ), + ); +} diff --git a/lib/chat/views/no_conversation_widget.dart b/lib/chat/views/no_conversation_widget.dart new file mode 100644 index 0000000..4657966 --- /dev/null +++ b/lib/chat/views/no_conversation_widget.dart @@ -0,0 +1,35 @@ +import 'package:flutter/material.dart'; + +//XXX should rename this +class NoContactWidget extends StatelessWidget { + const NoContactWidget({super.key}); + + @override + // ignore: prefer_expression_function_bodies + Widget build( + BuildContext context, + ) => + Container( + width: double.infinity, + height: double.infinity, + decoration: BoxDecoration( + color: Theme.of(context).primaryColor, + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.emoji_people_outlined, + color: Theme.of(context).disabledColor, + size: 48, + ), + Text( + 'Choose A Conversation To Chat', + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: Theme.of(context).disabledColor, + ), + ), + ], + ), + ); +} diff --git a/lib/chat/views/views.dart b/lib/chat/views/views.dart new file mode 100644 index 0000000..b63153b --- /dev/null +++ b/lib/chat/views/views.dart @@ -0,0 +1 @@ +export 'build_chat_component.dart'; diff --git a/lib/old_to_refactor/managers/contact_list_manager.dart b/lib/chat_list/chat_list.dart similarity index 100% rename from lib/old_to_refactor/managers/contact_list_manager.dart rename to lib/chat_list/chat_list.dart diff --git a/lib/old_to_refactor/components/chat_single_contact_item_widget.dart b/lib/chat_list/views/chat_single_contact_item_widget.dart similarity index 100% rename from lib/old_to_refactor/components/chat_single_contact_item_widget.dart rename to lib/chat_list/views/chat_single_contact_item_widget.dart diff --git a/lib/old_to_refactor/components/chat_single_contact_list_widget.dart b/lib/chat_list/views/chat_single_contact_list_widget.dart similarity index 100% rename from lib/old_to_refactor/components/chat_single_contact_list_widget.dart rename to lib/chat_list/views/chat_single_contact_list_widget.dart diff --git a/lib/old_to_refactor/components/empty_chat_list_widget.dart b/lib/chat_list/views/empty_chat_list_widget.dart similarity index 79% rename from lib/old_to_refactor/components/empty_chat_list_widget.dart rename to lib/chat_list/views/empty_chat_list_widget.dart index 3ef0f97..024cbf0 100644 --- a/lib/old_to_refactor/components/empty_chat_list_widget.dart +++ b/lib/chat_list/views/empty_chat_list_widget.dart @@ -1,15 +1,14 @@ import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_translate/flutter_translate.dart'; -import '../tools/tools.dart'; +import '../../theme/theme.dart'; -class EmptyChatListWidget extends ConsumerWidget { +class EmptyChatListWidget extends StatelessWidget { const EmptyChatListWidget({super.key}); @override // ignore: prefer_expression_function_bodies - Widget build(BuildContext context, WidgetRef ref) { + Widget build(BuildContext context) { final theme = Theme.of(context); final textTheme = theme.textTheme; final scale = theme.extension()!; diff --git a/lib/chat_list/views/views.dart b/lib/chat_list/views/views.dart new file mode 100644 index 0000000..e69de29 diff --git a/lib/contact_invitation/contact_invitation.dart b/lib/contact_invitation/contact_invitation.dart new file mode 100644 index 0000000..83d1303 --- /dev/null +++ b/lib/contact_invitation/contact_invitation.dart @@ -0,0 +1 @@ +export 'views/views.dart'; diff --git a/lib/old_to_refactor/components/paste_invite_dialog.dart b/lib/contact_invitation/paste_invite_dialog.dart similarity index 96% rename from lib/old_to_refactor/components/paste_invite_dialog.dart rename to lib/contact_invitation/paste_invite_dialog.dart index b7e545c..a352892 100644 --- a/lib/old_to_refactor/components/paste_invite_dialog.dart +++ b/lib/contact_invitation/paste_invite_dialog.dart @@ -6,9 +6,9 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_translate/flutter_translate.dart'; -import '../tools/tools.dart'; -import '../veilid_support/veilid_support.dart'; -import 'invite_dialog.dart'; +import '../old_to_refactor/tools/tools.dart'; +import '../old_to_refactor/veilid_support/veilid_support.dart'; +import 'views/invite_dialog.dart'; class PasteInviteDialog extends ConsumerStatefulWidget { const PasteInviteDialog({super.key}); diff --git a/lib/old_to_refactor/components/scan_invite_dialog.dart b/lib/contact_invitation/scan_invite_dialog.dart similarity index 99% rename from lib/old_to_refactor/components/scan_invite_dialog.dart rename to lib/contact_invitation/scan_invite_dialog.dart index a506bcf..c7b4f76 100644 --- a/lib/old_to_refactor/components/scan_invite_dialog.dart +++ b/lib/contact_invitation/scan_invite_dialog.dart @@ -13,8 +13,8 @@ import 'package:mobile_scanner/mobile_scanner.dart'; import 'package:pasteboard/pasteboard.dart'; import 'package:zxing2/qrcode.dart'; -import '../tools/tools.dart'; -import 'invite_dialog.dart'; +import '../old_to_refactor/tools/tools.dart'; +import 'views/invite_dialog.dart'; class BarcodeOverlay extends CustomPainter { BarcodeOverlay({ diff --git a/lib/old_to_refactor/components/contact_invitation_display.dart b/lib/contact_invitation/views/contact_invitation_display.dart similarity index 95% rename from lib/old_to_refactor/components/contact_invitation_display.dart rename to lib/contact_invitation/views/contact_invitation_display.dart index 5f32ca8..dfd2ebf 100644 --- a/lib/old_to_refactor/components/contact_invitation_display.dart +++ b/lib/contact_invitation/views/contact_invitation_display.dart @@ -6,14 +6,13 @@ import 'package:basic_utils/basic_utils.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:qr_flutter/qr_flutter.dart'; +import 'package:veilid_support/veilid_support.dart'; -import '../tools/tools.dart'; -import '../veilid_support/veilid_support.dart'; +import '../../tools/tools.dart'; -class ContactInvitationDisplayDialog extends ConsumerStatefulWidget { +class ContactInvitationDisplayDialog extends StatefulWidget { const ContactInvitationDisplayDialog({ required this.name, required this.message, @@ -40,7 +39,7 @@ class ContactInvitationDisplayDialog extends ConsumerStatefulWidget { } class ContactInvitationDisplayDialogState - extends ConsumerState { + extends State { final focusNode = FocusNode(); final formKey = GlobalKey(); late final AutoDisposeFutureProvider _generateFutureProvider; diff --git a/lib/old_to_refactor/components/contact_invitation_item_widget.dart b/lib/contact_invitation/views/contact_invitation_item_widget.dart similarity index 94% rename from lib/old_to_refactor/components/contact_invitation_item_widget.dart rename to lib/contact_invitation/views/contact_invitation_item_widget.dart index 1a967ba..5b5b9ef 100644 --- a/lib/old_to_refactor/components/contact_invitation_item_widget.dart +++ b/lib/contact_invitation/views/contact_invitation_item_widget.dart @@ -1,15 +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 '../proto/proto.dart' as proto; -import '../providers/account.dart'; -import '../providers/contact_invite.dart'; -import '../tools/tools.dart'; +import '../../proto/proto.dart' as proto; +import '../../theme/theme.dart'; import 'contact_invitation_display.dart'; -class ContactInvitationItemWidget extends ConsumerWidget { +class ContactInvitationItemWidget extends StatelessWidget { const ContactInvitationItemWidget( {required this.contactInvitationRecord, super.key}); @@ -24,7 +21,7 @@ class ContactInvitationItemWidget extends ConsumerWidget { @override // ignore: prefer_expression_function_bodies - Widget build(BuildContext context, WidgetRef ref) { + Widget build(BuildContext context) { final theme = Theme.of(context); //final textTheme = theme.textTheme; final scale = theme.extension()!; diff --git a/lib/old_to_refactor/components/contact_invitation_list_widget.dart b/lib/contact_invitation/views/contact_invitation_list_widget.dart similarity index 91% rename from lib/old_to_refactor/components/contact_invitation_list_widget.dart rename to lib/contact_invitation/views/contact_invitation_list_widget.dart index 372a1cc..e93f746 100644 --- a/lib/old_to_refactor/components/contact_invitation_list_widget.dart +++ b/lib/contact_invitation/views/contact_invitation_list_widget.dart @@ -2,13 +2,12 @@ import 'package:awesome_extensions/awesome_extensions.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import '../proto/proto.dart' as proto; -import '../tools/tools.dart'; +import '../../proto/proto.dart' as proto; +import '../../theme/theme.dart'; import 'contact_invitation_item_widget.dart'; -class ContactInvitationListWidget extends ConsumerStatefulWidget { +class ContactInvitationListWidget extends StatefulWidget { const ContactInvitationListWidget({ required this.contactInvitationRecordList, super.key, @@ -28,7 +27,7 @@ class ContactInvitationListWidget extends ConsumerStatefulWidget { } class ContactInvitationListWidgetState - extends ConsumerState { + extends State { final ScrollController _scrollController = ScrollController(); @override diff --git a/lib/old_to_refactor/components/invite_dialog.dart b/lib/contact_invitation/views/invite_dialog.dart similarity index 96% rename from lib/old_to_refactor/components/invite_dialog.dart rename to lib/contact_invitation/views/invite_dialog.dart index 870c6fe..dc38361 100644 --- a/lib/old_to_refactor/components/invite_dialog.dart +++ b/lib/contact_invitation/views/invite_dialog.dart @@ -3,19 +3,12 @@ import 'dart:async'; import 'package:awesome_extensions/awesome_extensions.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_translate/flutter_translate.dart'; -import '../entities/local_account.dart'; -import '../providers/account.dart'; -import '../providers/contact.dart'; -import '../providers/contact_invite.dart'; -import '../tools/tools.dart'; -import 'enter_password.dart'; -import 'enter_pin.dart'; -import 'profile_widget.dart'; +import '../../account_manager/account_manager.dart'; +import '../../tools/tools.dart'; -class InviteDialog extends ConsumerStatefulWidget { +class InviteDialog extends StatefulWidget { const InviteDialog( {required this.onValidationCancelled, required this.onValidationSuccess, @@ -58,7 +51,7 @@ class InviteDialog extends ConsumerStatefulWidget { } } -class InviteDialogState extends ConsumerState { +class InviteDialogState extends State { ValidContactInvitation? _validInvitation; bool _isValidating = false; bool _isAccepting = false; diff --git a/lib/contact_invitation/views/new_contact_invitation_bottom_sheet.dart b/lib/contact_invitation/views/new_contact_invitation_bottom_sheet.dart new file mode 100644 index 0000000..9430146 --- /dev/null +++ b/lib/contact_invitation/views/new_contact_invitation_bottom_sheet.dart @@ -0,0 +1,65 @@ +import 'package:awesome_extensions/awesome_extensions.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_translate/flutter_translate.dart'; + +import '../../theme/theme.dart'; +import 'paste_invite_dialog.dart'; +import 'scan_invite_dialog.dart'; +import 'send_invite_dialog.dart'; + +Widget newContactInvitationBottomSheetBuilder(BuildContext context) { + final theme = Theme.of(context); + final textTheme = theme.textTheme; + final scale = theme.extension()!; + + 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: textTheme.titleMedium) + .paddingAll(8), + Row(mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ + Column(children: [ + IconButton( + onPressed: () async { + Navigator.pop(context); + await SendInviteDialog.show(context); + }, + iconSize: 64, + icon: const Icon(Icons.contact_page), + color: scale.primaryScale.background), + Text(translate('accounts_menu.create_invite')) + ]), + Column(children: [ + IconButton( + onPressed: () async { + Navigator.pop(context); + await ScanInviteDialog.show(context); + }, + iconSize: 64, + icon: const Icon(Icons.qr_code_scanner), + color: scale.primaryScale.background), + Text(translate('accounts_menu.scan_invite')) + ]), + Column(children: [ + IconButton( + onPressed: () async { + Navigator.pop(context); + await PasteInviteDialog.show(context); + }, + iconSize: 64, + icon: const Icon(Icons.paste), + color: scale.primaryScale.background), + Text(translate('accounts_menu.paste_invite')) + ]) + ]).expanded() + ]))); +} diff --git a/lib/contact_invitation/views/paste_invite_dialog.dart b/lib/contact_invitation/views/paste_invite_dialog.dart new file mode 100644 index 0000000..545a48a --- /dev/null +++ b/lib/contact_invitation/views/paste_invite_dialog.dart @@ -0,0 +1,131 @@ +import 'dart:async'; +import 'dart:typed_data'; + +import 'package:awesome_extensions/awesome_extensions.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_translate/flutter_translate.dart'; +import 'package:veilid_support/veilid_support.dart'; + +import '../../theme/theme.dart'; +import '../../tools/tools.dart'; +import 'invite_dialog.dart'; + +class PasteInviteDialog extends StatefulWidget { + const PasteInviteDialog({super.key}); + + @override + PasteInviteDialogState createState() => PasteInviteDialogState(); + + static Future show(BuildContext context) async { + await showStyledDialog( + context: context, + title: translate('paste_invite_dialog.title'), + child: const PasteInviteDialog()); + } +} + +class PasteInviteDialogState extends State { + final _pasteTextController = TextEditingController(); + + @override + void initState() { + super.initState(); + } + + Future _onPasteChanged( + String text, + Future Function({ + required Uint8List inviteData, + }) validateInviteData) async { + final lines = text.split('\n'); + if (lines.isEmpty) { + return; + } + + var firstline = + lines.indexWhere((element) => element.contains('BEGIN VEILIDCHAT')); + firstline += 1; + + var lastline = + lines.indexWhere((element) => element.contains('END VEILIDCHAT')); + if (lastline == -1) { + lastline = lines.length; + } + if (lastline <= firstline) { + return; + } + final inviteDataBase64 = lines + .sublist(firstline, lastline) + .join() + .replaceAll(RegExp(r'[^A-Za-z0-9\-_]'), ''); + final inviteData = base64UrlNoPadDecode(inviteDataBase64); + + await validateInviteData(inviteData: inviteData); + } + + void onValidationCancelled() { + _pasteTextController.clear(); + } + + void onValidationSuccess() { + //_pasteTextController.clear(); + } + + void onValidationFailed() { + _pasteTextController.clear(); + } + + bool inviteControlIsValid() => _pasteTextController.text.isNotEmpty; + + Widget buildInviteControl( + BuildContext context, + InviteDialogState dialogState, + Future Function({required Uint8List inviteData}) + validateInviteData) { + final theme = Theme.of(context); + final scale = theme.extension()!; + //final textTheme = theme.textTheme; + //final height = MediaQuery.of(context).size.height; + + final monoStyle = TextStyle( + fontFamily: 'Source Code Pro', + fontSize: 11, + color: scale.primaryScale.text, + ); + + return Column(mainAxisSize: MainAxisSize.min, children: [ + Text( + translate('paste_invite_dialog.paste_invite_here'), + ).paddingLTRB(0, 0, 0, 8), + Container( + constraints: const BoxConstraints(maxHeight: 200), + child: TextField( + enabled: !dialogState.isValidating, + onChanged: (text) async => + _onPasteChanged(text, validateInviteData), + style: monoStyle, + keyboardType: TextInputType.multiline, + maxLines: null, + controller: _pasteTextController, + decoration: const InputDecoration( + border: OutlineInputBorder(), + hintText: '--- BEGIN VEILIDCHAT CONTACT INVITE ----\n' + 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX\n' + '---- END VEILIDCHAT CONTACT INVITE -----\n', + //labelText: translate('paste_invite_dialog.paste') + ), + )).paddingLTRB(0, 0, 0, 8) + ]); + } + + @override + // ignore: prefer_expression_function_bodies + Widget build(BuildContext context) { + return InviteDialog( + onValidationCancelled: onValidationCancelled, + onValidationSuccess: onValidationSuccess, + onValidationFailed: onValidationFailed, + inviteControlIsValid: inviteControlIsValid, + buildInviteControl: buildInviteControl); + } +} diff --git a/lib/contact_invitation/views/scan_invite_dialog.dart b/lib/contact_invitation/views/scan_invite_dialog.dart new file mode 100644 index 0000000..67c0999 --- /dev/null +++ b/lib/contact_invitation/views/scan_invite_dialog.dart @@ -0,0 +1,395 @@ +import 'dart:async'; +import 'dart:io'; +import 'dart:typed_data'; + +import 'package:awesome_extensions/awesome_extensions.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/scheduler.dart'; +import 'package:flutter_translate/flutter_translate.dart'; +import 'package:image/image.dart' as img; +import 'package:mobile_scanner/mobile_scanner.dart'; +import 'package:pasteboard/pasteboard.dart'; +import 'package:zxing2/qrcode.dart'; + +import '../../theme/theme.dart'; +import '../../tools/tools.dart'; +import 'invite_dialog.dart'; + +class BarcodeOverlay extends CustomPainter { + BarcodeOverlay({ + required this.barcode, + required this.arguments, + required this.boxFit, + required this.capture, + }); + + final BarcodeCapture capture; + final Barcode barcode; + final MobileScannerArguments arguments; + final BoxFit boxFit; + + @override + void paint(Canvas canvas, Size size) { + final adjustedSize = applyBoxFit(boxFit, arguments.size, size); + + var verticalPadding = size.height - adjustedSize.destination.height; + var horizontalPadding = size.width - adjustedSize.destination.width; + if (verticalPadding > 0) { + verticalPadding = verticalPadding / 2; + } else { + verticalPadding = 0; + } + + if (horizontalPadding > 0) { + horizontalPadding = horizontalPadding / 2; + } else { + horizontalPadding = 0; + } + + final ratioWidth = (Platform.isIOS ? capture.width : arguments.size.width) / + adjustedSize.destination.width; + final ratioHeight = + (Platform.isIOS ? capture.height : arguments.size.height) / + adjustedSize.destination.height; + + final adjustedOffset = []; + for (final offset in barcode.corners) { + adjustedOffset.add( + Offset( + offset.dx / ratioWidth + horizontalPadding, + offset.dy / ratioHeight + verticalPadding, + ), + ); + } + final cutoutPath = Path()..addPolygon(adjustedOffset, true); + + final backgroundPaint = Paint() + ..color = Colors.red.withOpacity(0.3) + ..style = PaintingStyle.fill + ..blendMode = BlendMode.dstOut; + + canvas.drawPath(cutoutPath, backgroundPaint); + } + + @override + bool shouldRepaint(covariant CustomPainter oldDelegate) => false; +} + +class ScannerOverlay extends CustomPainter { + ScannerOverlay(this.scanWindow); + + final Rect scanWindow; + + @override + void paint(Canvas canvas, Size size) { + final backgroundPath = Path()..addRect(Rect.largest); + final cutoutPath = Path()..addRect(scanWindow); + + final backgroundPaint = Paint() + ..color = Colors.black.withOpacity(0.5) + ..style = PaintingStyle.fill + ..blendMode = BlendMode.dstOut; + + final backgroundWithCutout = Path.combine( + PathOperation.difference, + backgroundPath, + cutoutPath, + ); + canvas.drawPath(backgroundWithCutout, backgroundPaint); + } + + @override + bool shouldRepaint(covariant CustomPainter oldDelegate) => false; +} + +class ScanInviteDialog extends StatefulWidget { + const ScanInviteDialog({super.key}); + + @override + ScanInviteDialogState createState() => ScanInviteDialogState(); + + static Future show(BuildContext context) async { + await showStyledDialog( + context: context, + title: translate('scan_invite_dialog.title'), + child: const ScanInviteDialog()); + } +} + +class ScanInviteDialogState extends State { + bool scanned = false; + + @override + void initState() { + super.initState(); + } + + void onValidationCancelled() { + setState(() { + scanned = false; + }); + } + + void onValidationSuccess() {} + void onValidationFailed() { + setState(() { + scanned = false; + }); + } + + bool inviteControlIsValid() => false; // _pasteTextController.text.isNotEmpty; + + Future scanQRImage(BuildContext context) async { + final theme = Theme.of(context); + //final textTheme = theme.textTheme; + final scale = theme.extension()!; + final windowSize = MediaQuery.of(context).size; + //final maxDialogWidth = min(windowSize.width - 64.0, 800.0 - 64.0); + //final maxDialogHeight = windowSize.height - 64.0; + + final scanWindow = Rect.fromCenter( + center: MediaQuery.of(context).size.center(Offset.zero), + width: 200, + height: 200, + ); + + final cameraController = MobileScannerController(); + try { + return showDialog( + context: context, + builder: (context) => Stack( + fit: StackFit.expand, + children: [ + MobileScanner( + fit: BoxFit.contain, + scanWindow: scanWindow, + controller: cameraController, + errorBuilder: (context, error, child) => + ScannerErrorWidget(error: error), + onDetect: (c) { + final barcode = c.barcodes.firstOrNull; + + final barcodeBytes = barcode?.rawBytes; + if (barcodeBytes != null) { + cameraController.dispose(); + Navigator.pop(context, barcodeBytes); + } + }), + CustomPaint( + painter: ScannerOverlay(scanWindow), + ), + Align( + alignment: Alignment.bottomCenter, + child: Container( + alignment: Alignment.bottomCenter, + height: 100, + color: Colors.black.withOpacity(0.4), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + IconButton( + color: Colors.white, + icon: ValueListenableBuilder( + valueListenable: cameraController.torchState, + builder: (context, state, child) { + switch (state) { + case TorchState.off: + return Icon(Icons.flash_off, + color: + scale.grayScale.subtleBackground); + case TorchState.on: + return Icon(Icons.flash_on, + color: scale.primaryScale.background); + } + }, + ), + iconSize: 32, + onPressed: cameraController.toggleTorch, + ), + SizedBox( + width: windowSize.width - 120, + height: 50, + child: FittedBox( + child: Text( + translate('scan_invite_dialog.instructions'), + overflow: TextOverflow.fade, + style: Theme.of(context) + .textTheme + .labelLarge! + .copyWith(color: Colors.white), + ), + ), + ), + IconButton( + color: Colors.white, + icon: ValueListenableBuilder( + valueListenable: + cameraController.cameraFacingState, + builder: (context, state, child) { + switch (state) { + case CameraFacing.front: + return const Icon(Icons.camera_front); + case CameraFacing.back: + return const Icon(Icons.camera_rear); + } + }, + ), + iconSize: 32, + onPressed: cameraController.switchCamera, + ), + ], + ), + ), + ), + Align( + alignment: Alignment.topRight, + child: IconButton( + color: Colors.white, + icon: Icon(Icons.close, + color: scale.grayScale.background), + iconSize: 32, + onPressed: () => { + SchedulerBinding.instance + .addPostFrameCallback((_) { + cameraController.dispose(); + Navigator.pop(context, null); + }) + })), + ], + )); + } on MobileScannerException catch (e) { + if (e.errorCode == MobileScannerErrorCode.permissionDenied) { + showErrorToast( + context, translate('scan_invite_dialog.permission_error')); + } else { + showErrorToast(context, translate('scan_invite_dialog.error')); + } + } on Exception catch (_) { + showErrorToast(context, translate('scan_invite_dialog.error')); + } + + return null; + } + + Future pasteQRImage(BuildContext context) async { + final imageBytes = await Pasteboard.image; + if (imageBytes == null) { + if (context.mounted) { + showErrorToast(context, translate('scan_invite_dialog.not_an_image')); + } + return null; + } + + final image = img.decodeImage(imageBytes); + if (image == null) { + if (context.mounted) { + showErrorToast( + context, translate('scan_invite_dialog.could_not_decode_image')); + } + return null; + } + + try { + final source = RGBLuminanceSource( + image.width, + image.height, + image + .convert(numChannels: 4) + .getBytes(order: img.ChannelOrder.abgr) + .buffer + .asInt32List()); + final bitmap = BinaryBitmap(HybridBinarizer(source)); + + final reader = QRCodeReader(); + final result = reader.decode(bitmap); + + final segs = result.resultMetadata[ResultMetadataType.byteSegments]! + as List; + return Uint8List.fromList(segs[0].toList()); + } on Exception catch (_) { + if (context.mounted) { + showErrorToast( + context, translate('scan_invite_dialog.not_a_valid_qr_code')); + } + return null; + } + } + + Widget buildInviteControl( + BuildContext context, + InviteDialogState dialogState, + Future Function({required Uint8List inviteData}) + validateInviteData) { + //final theme = Theme.of(context); + //final scale = theme.extension()!; + //final textTheme = theme.textTheme; + //final height = MediaQuery.of(context).size.height; + + if (isiOS || isAndroid) { + return Column(mainAxisSize: MainAxisSize.min, children: [ + if (!scanned) + Text( + translate('scan_invite_dialog.scan_qr_here'), + ).paddingLTRB(0, 0, 0, 8), + if (!scanned) + Container( + constraints: const BoxConstraints(maxHeight: 200), + child: ElevatedButton( + onPressed: dialogState.isValidating + ? null + : () async { + final inviteData = await scanQRImage(context); + if (inviteData != null) { + setState(() { + scanned = true; + }); + await validateInviteData(inviteData: inviteData); + } + }, + child: Text(translate('scan_invite_dialog.scan'))), + ).paddingLTRB(0, 0, 0, 8) + ]); + } + return Column(mainAxisSize: MainAxisSize.min, children: [ + if (!scanned) + Text( + translate('scan_invite_dialog.paste_qr_here'), + ).paddingLTRB(0, 0, 0, 8), + if (!scanned) + Container( + constraints: const BoxConstraints(maxHeight: 200), + child: ElevatedButton( + onPressed: dialogState.isValidating + ? null + : () async { + final inviteData = await pasteQRImage(context); + if (inviteData != null) { + await validateInviteData(inviteData: inviteData); + setState(() { + scanned = true; + }); + } + }, + child: Text(translate('scan_invite_dialog.paste'))), + ).paddingLTRB(0, 0, 0, 8) + ]); + } + + @override + // ignore: prefer_expression_function_bodies + Widget build(BuildContext context) { + return InviteDialog( + onValidationCancelled: onValidationCancelled, + onValidationSuccess: onValidationSuccess, + onValidationFailed: onValidationFailed, + inviteControlIsValid: inviteControlIsValid, + buildInviteControl: buildInviteControl); + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.add(DiagnosticsProperty('scanned', scanned)); + } +} diff --git a/lib/old_to_refactor/components/send_invite_dialog.dart b/lib/contact_invitation/views/send_invite_dialog.dart similarity index 93% rename from lib/old_to_refactor/components/send_invite_dialog.dart rename to lib/contact_invitation/views/send_invite_dialog.dart index 49adb68..417a18a 100644 --- a/lib/old_to_refactor/components/send_invite_dialog.dart +++ b/lib/contact_invitation/views/send_invite_dialog.dart @@ -5,19 +5,14 @@ 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:veilid_support/veilid_support.dart'; -import '../entities/local_account.dart'; -import '../providers/account.dart'; -import '../providers/contact_invite.dart'; -import '../tools/tools.dart'; -import '../veilid_support/veilid_support.dart'; +import '../../account_manager/account_manager.dart'; +import '../../tools/tools.dart'; import 'contact_invitation_display.dart'; -import 'enter_password.dart'; -import 'enter_pin.dart'; -class SendInviteDialog extends ConsumerStatefulWidget { +class SendInviteDialog extends StatefulWidget { const SendInviteDialog({super.key}); @override @@ -31,7 +26,7 @@ class SendInviteDialog extends ConsumerStatefulWidget { } } -class SendInviteDialogState extends ConsumerState { +class SendInviteDialogState extends State { final _messageTextController = TextEditingController( text: translate('send_invite_dialog.connect_with_me')); @@ -135,7 +130,8 @@ class SendInviteDialogState extends ConsumerState { final navigator = Navigator.of(context); // Start generation - final activeAccountInfo = await ref.read(fetchActiveAccountProvider.future); + final activeAccountInfo = + await AccountRepository.instance.fetchActiveAccountInfo(); if (activeAccountInfo == null) { navigator.pop(); return; diff --git a/lib/contact_invitation/views/views.dart b/lib/contact_invitation/views/views.dart new file mode 100644 index 0000000..d9f599b --- /dev/null +++ b/lib/contact_invitation/views/views.dart @@ -0,0 +1,8 @@ +export 'contact_invitation_display.dart'; +export 'contact_invitation_item_widget.dart'; +export 'contact_invitation_list_widget.dart'; +export 'invite_dialog.dart'; +export 'new_contact_invitation_bottom_sheet.dart'; +export 'paste_invite_dialog.dart'; +export 'scan_invite_dialog.dart'; +export 'send_invite_dialog.dart'; diff --git a/lib/contacts/contacts.dart b/lib/contacts/contacts.dart new file mode 100644 index 0000000..83d1303 --- /dev/null +++ b/lib/contacts/contacts.dart @@ -0,0 +1 @@ +export 'views/views.dart'; diff --git a/lib/old_to_refactor/components/contact_item_widget.dart b/lib/contacts/views/contact_item_widget.dart similarity index 92% rename from lib/old_to_refactor/components/contact_item_widget.dart rename to lib/contacts/views/contact_item_widget.dart index ed57997..5ffaaa4 100644 --- a/lib/old_to_refactor/components/contact_item_widget.dart +++ b/lib/contacts/views/contact_item_widget.dart @@ -1,24 +1,21 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_animate/flutter_animate.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_slidable/flutter_slidable.dart'; import 'package:flutter_translate/flutter_translate.dart'; -import '../proto/proto.dart' as proto; -import '../pages/main_pager/main_pager.dart'; -import '../providers/account.dart'; -import '../providers/chat.dart'; -import '../providers/contact.dart'; -import '../theme/theme.dart'; +import '../../proto/proto.dart' as proto; +import '../../theme/theme.dart'; -class ContactItemWidget extends ConsumerWidget { +class ContactItemWidget extends StatelessWidget { const ContactItemWidget({required this.contact, super.key}); final proto.Contact contact; @override // ignore: prefer_expression_function_bodies - Widget build(BuildContext context, WidgetRef ref) { + Widget build( + BuildContext context, + ) { final theme = Theme.of(context); //final textTheme = theme.textTheme; final scale = theme.extension()!; diff --git a/lib/old_to_refactor/components/contact_list_widget.dart b/lib/contacts/views/contact_list_widget.dart similarity index 100% rename from lib/old_to_refactor/components/contact_list_widget.dart rename to lib/contacts/views/contact_list_widget.dart diff --git a/lib/old_to_refactor/components/empty_contact_list_widget.dart b/lib/contacts/views/empty_contact_list_widget.dart similarity index 80% rename from lib/old_to_refactor/components/empty_contact_list_widget.dart rename to lib/contacts/views/empty_contact_list_widget.dart index bcd832b..db07b4a 100644 --- a/lib/old_to_refactor/components/empty_contact_list_widget.dart +++ b/lib/contacts/views/empty_contact_list_widget.dart @@ -1,15 +1,16 @@ import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_translate/flutter_translate.dart'; -import '../tools/tools.dart'; +import '../../theme/theme.dart'; -class EmptyContactListWidget extends ConsumerWidget { +class EmptyContactListWidget extends StatelessWidget { const EmptyContactListWidget({super.key}); @override // ignore: prefer_expression_function_bodies - Widget build(BuildContext context, WidgetRef ref) { + Widget build( + BuildContext context, + ) { final theme = Theme.of(context); final textTheme = theme.textTheme; final scale = theme.extension()!; diff --git a/lib/contacts/views/views.dart b/lib/contacts/views/views.dart new file mode 100644 index 0000000..8c98b0f --- /dev/null +++ b/lib/contacts/views/views.dart @@ -0,0 +1,3 @@ +export 'contact_item_widget.dart'; +export 'contact_list_widget.dart'; +export 'empty_contact_list_widget.dart'; diff --git a/lib/layout/chat_only.dart b/lib/layout/chat_only.dart index ad81b4c..6f4e645 100644 --- a/lib/layout/chat_only.dart +++ b/lib/layout/chat_only.dart @@ -1,8 +1,7 @@ import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import '../providers/window_control.dart'; -import 'home.dart'; +import '../chat/chat.dart'; +import '../tools/tools.dart'; class ChatOnlyPage extends StatefulWidget { const ChatOnlyPage({super.key}); @@ -11,7 +10,7 @@ class ChatOnlyPage extends StatefulWidget { ChatOnlyPageState createState() => ChatOnlyPageState(); } -class ChatOnlyPageState extends ConsumerState +class ChatOnlyPageState extends State with TickerProviderStateMixin { final _unfocusNode = FocusNode(); @@ -21,7 +20,7 @@ class ChatOnlyPageState extends ConsumerState WidgetsBinding.instance.addPostFrameCallback((_) async { setState(() {}); - await ref.read(windowControlProvider.notifier).changeWindowSetup( + await changeWindowSetup( TitleBarStyle.normal, OrientationCapability.normal); }); } @@ -33,13 +32,9 @@ class ChatOnlyPageState extends ConsumerState } @override - Widget build(BuildContext context) { - ref.watch(windowControlProvider); - - return SafeArea( - child: GestureDetector( - onTap: () => FocusScope.of(context).requestFocus(_unfocusNode), - child: HomePage.buildChatComponent(context, ref), - )); - } + Widget build(BuildContext context) => SafeArea( + child: GestureDetector( + onTap: () => FocusScope.of(context).requestFocus(_unfocusNode), + child: buildChatComponent(), + )); } diff --git a/lib/layout/edit_contact.dart b/lib/layout/edit_contact.dart index 169874f..480ff1f 100644 --- a/lib/layout/edit_contact.dart +++ b/lib/layout/edit_contact.dart @@ -1,28 +1,30 @@ import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -class ContactsPage extends ConsumerWidget { +class ContactsPage extends StatelessWidget { const ContactsPage({super.key}); static const path = '/contacts'; @override - Widget build(BuildContext context, WidgetRef ref) => const Scaffold( - body: Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text('Contacts Page'), - // ElevatedButton( - // onPressed: () async { - // ref.watch(authNotifierProvider.notifier).login( - // "myEmail", - // "myPassword", - // ); - // }, - // child: const Text("Login"), - // ), - ], + Widget build( + BuildContext context, + ) => + const Scaffold( + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text('Contacts Page'), + // ElevatedButton( + // onPressed: () async { + // ref.watch(authNotifierProvider.notifier).login( + // "myEmail", + // "myPassword", + // ); + // }, + // child: const Text("Login"), + // ), + ], + ), ), - ), - ); + ); } diff --git a/lib/layout/home.dart b/lib/layout/home.dart index 86e838f..eeec9dc 100644 --- a/lib/layout/home.dart +++ b/lib/layout/home.dart @@ -9,7 +9,7 @@ import 'package:veilid_support/veilid_support.dart'; import '../../proto/proto.dart' as proto; import '../account_manager/account_manager.dart'; -import '../account_manager/models/models.dart'; +import '../chat/chat.dart'; import '../theme/theme.dart'; import '../tools/tools.dart'; import 'main_pager/main_pager.dart'; @@ -19,38 +19,6 @@ class HomePage extends StatefulWidget { @override HomePageState createState() => HomePageState(); - - static Widget buildChatComponent() { - final contactList = ref.watch(fetchContactListProvider).asData?.value ?? - const IListConst([]); - - final activeChat = ref.watch(activeChatStateProvider); - if (activeChat == null) { - return const EmptyChatWidget(); - } - - final activeAccountInfo = - ref.watch(fetchActiveAccountProvider).asData?.value; - if (activeAccountInfo == null) { - return const EmptyChatWidget(); - } - - final activeChatContactIdx = contactList.indexWhere( - (c) => - proto.TypedKeyProto.fromProto(c.remoteConversationRecordKey) == - activeChat, - ); - if (activeChatContactIdx == -1) { - ref.read(activeChatStateProvider.notifier).state = null; - return const EmptyChatWidget(); - } - final activeChatContact = contactList[activeChatContactIdx]; - - return ChatComponent( - activeAccountInfo: activeAccountInfo, - activeChat: activeChat, - activeChatContact: activeChatContact); - } } class HomePageState extends State with TickerProviderStateMixin { @@ -196,7 +164,7 @@ class HomePageState extends State with TickerProviderStateMixin { Widget buildTabletLeftPane() => Material(color: Colors.transparent, child: buildUserPanel()); - Widget buildTabletRightPane() => HomePage.buildChatComponent(); + Widget buildTabletRightPane() => buildChatComponent(); // ignore: prefer_expression_function_bodies Widget buildTablet() => Builder(builder: (context) { diff --git a/lib/layout/main_pager/account.dart b/lib/layout/main_pager/account.dart index 553c288..ece96c1 100644 --- a/lib/layout/main_pager/account.dart +++ b/lib/layout/main_pager/account.dart @@ -4,20 +4,15 @@ import 'package:awesome_extensions/awesome_extensions.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_translate/flutter_translate.dart'; +import 'package:veilid_support/veilid_support.dart'; -import '../../../components/contact_invitation_list_widget.dart'; -import '../../../components/contact_list_widget.dart'; -import '../../../entities/local_account.dart'; import '../../../proto/proto.dart' as proto; -import '../../providers/contact.dart'; -import '../../providers/contact_invite.dart'; -import '../../../theme/theme.dart'; -import '../../../tools/tools.dart'; -import '../../../../packages/veilid_support/veilid_support.dart'; +import '../../account_manager/account_manager.dart'; +import '../../contact_invitation/contact_invitation.dart'; +import '../../contacts/contacts.dart'; -class AccountPage extends ConsumerStatefulWidget { +class AccountPage extends StatefulWidget { const AccountPage({ required this.localAccounts, required this.activeUserLogin, @@ -41,7 +36,7 @@ class AccountPage extends ConsumerStatefulWidget { } } -class AccountPageState extends ConsumerState { +class AccountPageState extends State { final _unfocusNode = FocusNode(); @override diff --git a/lib/old_to_refactor/components/bottom_sheet_action_button.dart b/lib/layout/main_pager/bottom_sheet_action_button.dart similarity index 90% rename from lib/old_to_refactor/components/bottom_sheet_action_button.dart rename to lib/layout/main_pager/bottom_sheet_action_button.dart index 4330d33..c34e478 100644 --- a/lib/old_to_refactor/components/bottom_sheet_action_button.dart +++ b/lib/layout/main_pager/bottom_sheet_action_button.dart @@ -1,8 +1,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -class BottomSheetActionButton extends ConsumerStatefulWidget { +class BottomSheetActionButton extends StatefulWidget { const BottomSheetActionButton( {required this.bottomSheetBuilder, required this.builder, @@ -32,8 +31,7 @@ class BottomSheetActionButton extends ConsumerStatefulWidget { } } -class BottomSheetActionButtonState - extends ConsumerState { +class BottomSheetActionButtonState extends State { bool _showFab = true; @override diff --git a/lib/layout/main_pager/chats.dart b/lib/layout/main_pager/chats.dart index d35fe6b..56756e2 100644 --- a/lib/layout/main_pager/chats.dart +++ b/lib/layout/main_pager/chats.dart @@ -1,28 +1,19 @@ import 'package:awesome_extensions/awesome_extensions.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import '../../../components/chat_single_contact_list_widget.dart'; -import '../../../components/empty_chat_list_widget.dart'; -import '../../../entities/local_account.dart'; import '../../../proto/proto.dart' as proto; -import '../../providers/account.dart'; -import '../../providers/chat.dart'; -import '../../providers/contact.dart'; -import '../../../local_accounts/local_accounts.dart'; -import '../../providers/logins.dart'; -import '../../../tools/tools.dart'; -import '../../../../packages/veilid_support/veilid_support.dart'; +import '../../account_manager/account_manager.dart'; +import '../../tools/tools.dart'; -class ChatsPage extends ConsumerStatefulWidget { +class ChatsPage extends StatefulWidget { const ChatsPage({super.key}); @override ChatsPageState createState() => ChatsPageState(); } -class ChatsPageState extends ConsumerState { +class ChatsPageState extends State { final _unfocusNode = FocusNode(); @override diff --git a/lib/layout/main_pager/main_pager.dart b/lib/layout/main_pager/main_pager.dart index 37c55fd..05e5979 100644 --- a/lib/layout/main_pager/main_pager.dart +++ b/lib/layout/main_pager/main_pager.dart @@ -1,27 +1,23 @@ import 'dart:async'; -import 'package:awesome_extensions/awesome_extensions.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:flutter/foundation.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:preload_page_view/preload_page_view.dart'; import 'package:stylish_bottom_bar/model/bar_items.dart'; import 'package:stylish_bottom_bar/stylish_bottom_bar.dart'; +import 'package:veilid_support/veilid_support.dart'; -import '../../../components/bottom_sheet_action_button.dart'; -import '../../../components/paste_invite_dialog.dart'; -import '../../../components/scan_invite_dialog.dart'; -import '../../../components/send_invite_dialog.dart'; -import '../../../entities/local_account.dart'; import '../../../proto/proto.dart' as proto; import '../../../tools/tools.dart'; -import '../../../../packages/veilid_support/veilid_support.dart'; +import '../../account_manager/account_manager.dart'; +import '../../contact_invitation/contact_invitation.dart'; +import '../../theme/theme.dart'; import 'account.dart'; +import 'bottom_sheet_action_button.dart'; import 'chats.dart'; class MainPager extends StatefulWidget { @@ -50,8 +46,7 @@ class MainPager extends StatefulWidget { } } -class MainPagerState extends ConsumerState - with TickerProviderStateMixin { +class MainPagerState extends State with TickerProviderStateMixin { ////////////////////////////////////////////////////////////////// final _unfocusNode = FocusNode(); @@ -151,64 +146,6 @@ class MainPagerState extends ConsumerState }); } - Widget _newContactInvitationBottomSheetBuilder( - // ignore: prefer_expression_function_bodies - BuildContext context) { - final theme = Theme.of(context); - final textTheme = theme.textTheme; - final scale = theme.extension()!; - - 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: textTheme.titleMedium) - .paddingAll(8), - Row(mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ - Column(children: [ - IconButton( - onPressed: () async { - Navigator.pop(context); - await SendInviteDialog.show(context); - }, - iconSize: 64, - icon: const Icon(Icons.contact_page), - color: scale.primaryScale.background), - Text(translate('accounts_menu.create_invite')) - ]), - Column(children: [ - IconButton( - onPressed: () async { - Navigator.pop(context); - await ScanInviteDialog.show(context); - }, - iconSize: 64, - icon: const Icon(Icons.qr_code_scanner), - color: scale.primaryScale.background), - Text(translate('accounts_menu.scan_invite')) - ]), - Column(children: [ - IconButton( - onPressed: () async { - Navigator.pop(context); - await PasteInviteDialog.show(context); - }, - iconSize: 64, - icon: const Icon(Icons.paste), - color: scale.primaryScale.background), - Text(translate('accounts_menu.paste_invite')) - ]) - ]).expanded() - ]))); - } - // ignore: prefer_expression_function_bodies Widget _onNewChatBottomSheetBuilder(BuildContext context) { return const SizedBox( @@ -221,7 +158,7 @@ class MainPagerState extends ConsumerState Widget _bottomSheetBuilder(BuildContext context) { if (_currentPage == 0) { // New contact invitation - return _newContactInvitationBottomSheetBuilder(context); + return newContactInvitationBottomSheetBuilder(context); } else if (_currentPage == 1) { // New chat return _onNewChatBottomSheetBuilder(context); diff --git a/lib/old_to_refactor/components/empty_chat_widget.dart b/lib/old_to_refactor/components/empty_chat_widget.dart deleted file mode 100644 index dbe184d..0000000 --- a/lib/old_to_refactor/components/empty_chat_widget.dart +++ /dev/null @@ -1,36 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; - -class EmptyChatWidget extends ConsumerWidget { - const EmptyChatWidget({super.key}); - - @override - // ignore: prefer_expression_function_bodies - Widget build(BuildContext context, WidgetRef ref) { - // - - return Container( - width: double.infinity, - height: double.infinity, - decoration: BoxDecoration( - color: Theme.of(context).scaffoldBackgroundColor, - ), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon( - Icons.chat, - color: Theme.of(context).disabledColor, - size: 48, - ), - Text( - 'Say Something', - style: Theme.of(context).textTheme.bodyMedium?.copyWith( - color: Theme.of(context).disabledColor, - ), - ), - ], - ), - ); - } -} diff --git a/lib/old_to_refactor/components/no_conversation_widget.dart b/lib/old_to_refactor/components/no_conversation_widget.dart deleted file mode 100644 index faf820f..0000000 --- a/lib/old_to_refactor/components/no_conversation_widget.dart +++ /dev/null @@ -1,35 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; - -class NoContactWidget extends ConsumerWidget { - const NoContactWidget({super.key}); - - @override - // ignore: prefer_expression_function_bodies - Widget build(BuildContext context, WidgetRef ref) { - // - return Container( - width: double.infinity, - height: double.infinity, - decoration: BoxDecoration( - color: Theme.of(context).primaryColor, - ), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon( - Icons.emoji_people_outlined, - color: Theme.of(context).disabledColor, - size: 48, - ), - Text( - 'Choose A Conversation To Chat', - style: Theme.of(context).textTheme.bodyMedium?.copyWith( - color: Theme.of(context).disabledColor, - ), - ), - ], - ), - ); - } -} diff --git a/lib/old_to_refactor/managers/valid_contact_invitation.dart b/lib/old_to_refactor/managers/valid_contact_invitation.dart deleted file mode 100644 index 8b13789..0000000 --- a/lib/old_to_refactor/managers/valid_contact_invitation.dart +++ /dev/null @@ -1 +0,0 @@ - diff --git a/lib/settings/preferences_cubit.dart b/lib/settings/preferences_cubit.dart index e9f69b5..5d85ce5 100644 --- a/lib/settings/preferences_cubit.dart +++ b/lib/settings/preferences_cubit.dart @@ -1,8 +1,6 @@ import '../tools/tools.dart'; import 'settings.dart'; -xxx convert to non-asyncvalue based wrapper since there's always a default here - class PreferencesCubit extends StreamWrapperCubit { PreferencesCubit(PreferencesRepository repository) : super(repository.stream, defaultState: repository.value); diff --git a/lib/settings/settings_page.dart b/lib/settings/settings_page.dart index e13fd3c..73c9ba7 100644 --- a/lib/settings/settings_page.dart +++ b/lib/settings/settings_page.dart @@ -1,17 +1,13 @@ import 'package:animated_theme_switcher/animated_theme_switcher.dart'; import 'package:awesome_extensions/awesome_extensions.dart'; -import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_form_builder/flutter_form_builder.dart'; import 'package:flutter_translate/flutter_translate.dart'; -import 'package:veilid_support/veilid_support.dart'; import '../layout/default_app_bar.dart'; import '../theme/theme.dart'; +import '../tools/tools.dart'; import '../veilid_processor/veilid_processor.dart'; -import 'preferences_cubit.dart'; -import 'preferences_repository.dart'; import 'settings.dart'; class SettingsPage extends StatefulWidget { @@ -68,8 +64,8 @@ class SettingsPageState extends State { } @override - Widget build(BuildContext context) => BlocBuilder>( + Widget build(BuildContext context) => AsyncBlocBuilder( builder: (context, state) => ThemeSwitchingArea( child: Scaffold( // resizeToAvoidBottomInset: false, @@ -94,14 +90,16 @@ class SettingsPageState extends State { label: Text(translate('settings_page.color_theme'))), items: _getThemeDropdownItems(), - initialValue: themePreferences.colorPreference, + initialValue: state.themePreferences.colorPreference, onChanged: (value) async { - final newPrefs = themePreferences.copyWith( - colorPreference: value as ColorPreference); - await themeService.save(newPrefs); + final newPrefs = state.copyWith( + themePreferences: state.themePreferences + .copyWith( + colorPreference: + value as ColorPreference)); switcher.changeTheme( - theme: themeService.get(newPrefs)); - ref.invalidate(themeServiceProvider); + theme: newPrefs.themePreferences.themeData()); + await PreferencesRepository.instance.set(newPrefs); setState(() {}); })), ThemeSwitcher.withTheme( @@ -111,15 +109,17 @@ class SettingsPageState extends State { label: Text( translate('settings_page.brightness_mode'))), items: _getBrightnessDropdownItems(), - initialValue: themePreferences.brightnessPreference, + initialValue: + state.themePreferences.brightnessPreference, onChanged: (value) async { - final newPrefs = themePreferences.copyWith( - brightnessPreference: - value as BrightnessPreference); - await themeService.save(newPrefs); + final newPrefs = state.copyWith( + themePreferences: state.themePreferences + .copyWith( + brightnessPreference: + value as BrightnessPreference)); switcher.changeTheme( - theme: themeService.get(newPrefs)); - ref.invalidate(themeServiceProvider); + theme: newPrefs.themePreferences.themeData()); + await PreferencesRepository.instance.set(newPrefs); setState(() {}); })), ], diff --git a/lib/old_to_refactor/components/enter_password.dart b/lib/tools/enter_password.dart similarity index 94% rename from lib/old_to_refactor/components/enter_password.dart rename to lib/tools/enter_password.dart index a1b06ab..42880ee 100644 --- a/lib/old_to_refactor/components/enter_password.dart +++ b/lib/tools/enter_password.dart @@ -2,12 +2,11 @@ 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 '../tools/tools.dart'; +import '../theme/theme.dart'; -class EnterPasswordDialog extends ConsumerStatefulWidget { +class EnterPasswordDialog extends StatefulWidget { const EnterPasswordDialog({ this.matchPass, this.description, @@ -29,7 +28,7 @@ class EnterPasswordDialog extends ConsumerStatefulWidget { } } -class EnterPasswordDialogState extends ConsumerState { +class EnterPasswordDialogState extends State { final passwordController = TextEditingController(); final focusNode = FocusNode(); final formKey = GlobalKey(); diff --git a/lib/old_to_refactor/components/enter_pin.dart b/lib/tools/enter_pin.dart similarity index 95% rename from lib/old_to_refactor/components/enter_pin.dart rename to lib/tools/enter_pin.dart index a8def81..3128710 100644 --- a/lib/old_to_refactor/components/enter_pin.dart +++ b/lib/tools/enter_pin.dart @@ -2,13 +2,12 @@ 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'; +import '../theme/theme.dart'; -class EnterPinDialog extends ConsumerStatefulWidget { +class EnterPinDialog extends StatefulWidget { const EnterPinDialog({ required this.reenter, required this.description, @@ -30,7 +29,7 @@ class EnterPinDialog extends ConsumerStatefulWidget { } } -class EnterPinDialogState extends ConsumerState { +class EnterPinDialogState extends State { final pinController = TextEditingController(); final focusNode = FocusNode(); final formKey = GlobalKey(); diff --git a/lib/tools/tools.dart b/lib/tools/tools.dart index 5532a56..6750c62 100644 --- a/lib/tools/tools.dart +++ b/lib/tools/tools.dart @@ -1,10 +1,13 @@ export 'animations.dart'; +export 'enter_password.dart'; +export 'enter_pin.dart'; export 'loggy.dart'; export 'phono_byte.dart'; export 'responsive.dart'; export 'scanner_error_widget.dart'; export 'shared_preferences.dart'; export 'state_logger.dart'; +export 'stream_listenable.dart'; export 'stream_wrapper_cubit.dart'; export 'widget_helpers.dart'; export 'window_control.dart'; diff --git a/lib/tools/widget_helpers.dart b/lib/tools/widget_helpers.dart index 90fb9bf..7812c5e 100644 --- a/lib/tools/widget_helpers.dart +++ b/lib/tools/widget_helpers.dart @@ -1,6 +1,7 @@ import 'package:awesome_extensions/awesome_extensions.dart'; import 'package:blurry_modal_progress_hud/blurry_modal_progress_hud.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_spinkit/flutter_spinkit.dart'; import 'package:flutter_translate/flutter_translate.dart'; import 'package:motion_toast/motion_toast.dart'; @@ -60,6 +61,22 @@ extension AsyncValueBuilderExt on AsyncValue { asyncValueBuilder(this, builder); } +class AsyncBlocBuilder>, S> + extends BlocBuilder> { + AsyncBlocBuilder({ + required BlocWidgetBuilder builder, + Widget Function(BuildContext)? loading, + Widget Function(BuildContext, Object, StackTrace?)? error, + super.key, + super.bloc, + super.buildWhen, + }) : super( + builder: (context, state) => state.when( + loading: () => (loading ?? waitingPage)(context), + error: (e, st) => (error ?? errorPage)(context, e, st), + data: (d) => builder(context, d))); +} + Future showErrorModal( BuildContext context, String title, String text) async { await QuickAlert.show( From 0a922e97b61f8510e643c016d168241354a1fe70 Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Thu, 18 Jan 2024 08:19:34 -0500 Subject: [PATCH 10/68] refactor settings page --- lib/settings/settings_page.dart | 124 +++++--------------- lib/theme/theme.dart | 1 + lib/theme/views/brightness_preferences.dart | 45 +++++++ lib/theme/views/color_preferences.dart | 55 +++++++++ lib/theme/views/views.dart | 2 + 5 files changed, 130 insertions(+), 97 deletions(-) create mode 100644 lib/theme/views/brightness_preferences.dart create mode 100644 lib/theme/views/color_preferences.dart create mode 100644 lib/theme/views/views.dart diff --git a/lib/settings/settings_page.dart b/lib/settings/settings_page.dart index 73c9ba7..b17b4a1 100644 --- a/lib/settings/settings_page.dart +++ b/lib/settings/settings_page.dart @@ -27,103 +27,33 @@ class SettingsPageState extends State { super.initState(); } - List> _getThemeDropdownItems() { - const colorPrefs = ColorPreference.values; - final colorNames = { - ColorPreference.scarlet: translate('themes.scarlet'), - ColorPreference.vapor: translate('themes.vapor'), - ColorPreference.babydoll: translate('themes.babydoll'), - ColorPreference.gold: translate('themes.gold'), - ColorPreference.garden: translate('themes.garden'), - ColorPreference.forest: translate('themes.forest'), - ColorPreference.arctic: translate('themes.arctic'), - ColorPreference.lapis: translate('themes.lapis'), - ColorPreference.eggplant: translate('themes.eggplant'), - ColorPreference.lime: translate('themes.lime'), - ColorPreference.grim: translate('themes.grim'), - ColorPreference.contrast: translate('themes.contrast') - }; - - return colorPrefs - .map((e) => DropdownMenuItem(value: e, child: Text(colorNames[e]!))) - .toList(); - } - - List> _getBrightnessDropdownItems() { - const brightnessPrefs = BrightnessPreference.values; - final brightnessNames = { - BrightnessPreference.system: translate('brightness.system'), - BrightnessPreference.light: translate('brightness.light'), - BrightnessPreference.dark: translate('brightness.dark') - }; - - return brightnessPrefs - .map( - (e) => DropdownMenuItem(value: e, child: Text(brightnessNames[e]!))) - .toList(); - } - @override - Widget build(BuildContext context) => AsyncBlocBuilder( - builder: (context, state) => ThemeSwitchingArea( - child: Scaffold( - // resizeToAvoidBottomInset: false, - appBar: DefaultAppBar( - title: Text(translate('settings_page.titlebar')), - leading: IconButton( - icon: const Icon(Icons.arrow_back), - onPressed: () => context.pop(), - ), - actions: [ - const SignalStrengthMeterWidget().paddingLTRB(16, 0, 16, 0), - ]), + Widget build(BuildContext context) => + AsyncBlocBuilder( + builder: (context, state) => ThemeSwitchingArea( + child: Scaffold( + // resizeToAvoidBottomInset: false, + appBar: DefaultAppBar( + title: Text(translate('settings_page.titlebar')), + leading: IconButton( + icon: const Icon(Icons.arrow_back), + onPressed: () => context.pop(), + ), + actions: [ + const SignalStrengthMeterWidget() + .paddingLTRB(16, 0, 16, 0), + ]), - body: FormBuilder( - key: _formKey, - child: ListView( - children: [ - ThemeSwitcher.withTheme( - builder: (_, switcher, theme) => FormBuilderDropdown( - name: formFieldTheme, - decoration: InputDecoration( - label: - Text(translate('settings_page.color_theme'))), - items: _getThemeDropdownItems(), - initialValue: state.themePreferences.colorPreference, - onChanged: (value) async { - final newPrefs = state.copyWith( - themePreferences: state.themePreferences - .copyWith( - colorPreference: - value as ColorPreference)); - switcher.changeTheme( - theme: newPrefs.themePreferences.themeData()); - await PreferencesRepository.instance.set(newPrefs); - setState(() {}); - })), - ThemeSwitcher.withTheme( - builder: (_, switcher, theme) => FormBuilderDropdown( - name: formFieldBrightness, - decoration: InputDecoration( - label: Text( - translate('settings_page.brightness_mode'))), - items: _getBrightnessDropdownItems(), - initialValue: - state.themePreferences.brightnessPreference, - onChanged: (value) async { - final newPrefs = state.copyWith( - themePreferences: state.themePreferences - .copyWith( - brightnessPreference: - value as BrightnessPreference)); - switcher.changeTheme( - theme: newPrefs.themePreferences.themeData()); - await PreferencesRepository.instance.set(newPrefs); - setState(() {}); - })), - ], - ), - ).paddingSymmetric(horizontal: 24, vertical: 8), - ))); + body: FormBuilder( + key: _formKey, + child: ListView( + children: [ + buildSettingsPageColorPreferences( + onChanged: () => setState(() {})), + buildSettingsPageBrightnessPreferences( + onChanged: () => setState(() {})), + ], + ), + ).paddingSymmetric(horizontal: 24, vertical: 8), + ))); } diff --git a/lib/theme/theme.dart b/lib/theme/theme.dart index 51b0215..3e9c176 100644 --- a/lib/theme/theme.dart +++ b/lib/theme/theme.dart @@ -1 +1,2 @@ export 'models/models.dart'; +export 'views/views.dart'; diff --git a/lib/theme/views/brightness_preferences.dart b/lib/theme/views/brightness_preferences.dart new file mode 100644 index 0000000..0c7ab10 --- /dev/null +++ b/lib/theme/views/brightness_preferences.dart @@ -0,0 +1,45 @@ +import 'package:animated_theme_switcher/animated_theme_switcher.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_form_builder/flutter_form_builder.dart'; +import 'package:flutter_translate/flutter_translate.dart'; + +import '../../settings/settings.dart'; +import '../models/models.dart'; + +const String formFieldBrightness = 'brightness'; + +List> _getBrightnessDropdownItems() { + const brightnessPrefs = BrightnessPreference.values; + final brightnessNames = { + BrightnessPreference.system: translate('brightness.system'), + BrightnessPreference.light: translate('brightness.light'), + BrightnessPreference.dark: translate('brightness.dark') + }; + + return brightnessPrefs + .map((e) => DropdownMenuItem(value: e, child: Text(brightnessNames[e]!))) + .toList(); +} + +Widget buildSettingsPageBrightnessPreferences( + {required void Function() onChanged}) { + final preferencesRepository = PreferencesRepository.instance; + final themePreferences = preferencesRepository.value.themePreferences; + return ThemeSwitcher.withTheme( + builder: (_, switcher, theme) => FormBuilderDropdown( + name: formFieldBrightness, + decoration: InputDecoration( + label: Text(translate('settings_page.brightness_mode'))), + items: _getBrightnessDropdownItems(), + initialValue: themePreferences.brightnessPreference, + onChanged: (value) async { + final newThemePrefs = themePreferences.copyWith( + brightnessPreference: value as BrightnessPreference); + final newPrefs = preferencesRepository.value + .copyWith(themePreferences: newThemePrefs); + + await preferencesRepository.set(newPrefs); + switcher.changeTheme(theme: newThemePrefs.themeData()); + onChanged(); + })); +} diff --git a/lib/theme/views/color_preferences.dart b/lib/theme/views/color_preferences.dart new file mode 100644 index 0000000..67e91f0 --- /dev/null +++ b/lib/theme/views/color_preferences.dart @@ -0,0 +1,55 @@ +import 'package:animated_theme_switcher/animated_theme_switcher.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_form_builder/flutter_form_builder.dart'; +import 'package:flutter_translate/flutter_translate.dart'; + +import '../../settings/settings.dart'; +import '../models/models.dart'; + +const String formFieldTheme = 'theme'; + +List> _getThemeDropdownItems() { + const colorPrefs = ColorPreference.values; + final colorNames = { + ColorPreference.scarlet: translate('themes.scarlet'), + ColorPreference.vapor: translate('themes.vapor'), + ColorPreference.babydoll: translate('themes.babydoll'), + ColorPreference.gold: translate('themes.gold'), + ColorPreference.garden: translate('themes.garden'), + ColorPreference.forest: translate('themes.forest'), + ColorPreference.arctic: translate('themes.arctic'), + ColorPreference.lapis: translate('themes.lapis'), + ColorPreference.eggplant: translate('themes.eggplant'), + ColorPreference.lime: translate('themes.lime'), + ColorPreference.grim: translate('themes.grim'), + ColorPreference.contrast: translate('themes.contrast') + }; + + return colorPrefs + .map((e) => DropdownMenuItem(value: e, child: Text(colorNames[e]!))) + .toList(); +} + +Widget buildSettingsPageColorPreferences({required void Function() onChanged}) { + final preferencesRepository = PreferencesRepository.instance; + final themePreferences = preferencesRepository.value.themePreferences; + return ThemeSwitcher.withTheme( + builder: (_, switcher, theme) => FormBuilderDropdown( + name: formFieldTheme, + decoration: InputDecoration( + label: Text(translate('settings_page.color_theme'))), + items: _getThemeDropdownItems(), + initialValue: themePreferences.colorPreference, + onChanged: (value) async { + final newThemePrefs = themePreferences.copyWith( + colorPreference: value as ColorPreference); + final newPrefs = preferencesRepository.value + .copyWith(themePreferences: newThemePrefs); + + await preferencesRepository.set(newPrefs); + switcher.changeTheme(theme: newThemePrefs.themeData()); + onChanged(); + + onChanged(); + })); +} diff --git a/lib/theme/views/views.dart b/lib/theme/views/views.dart new file mode 100644 index 0000000..85d06c4 --- /dev/null +++ b/lib/theme/views/views.dart @@ -0,0 +1,2 @@ +export 'brightness_preferences.dart'; +export 'color_preferences.dart'; From 6e8ba551ad101b69fb50413bd209669d37c75898 Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Thu, 18 Jan 2024 19:44:15 -0500 Subject: [PATCH 11/68] more refactor work --- .../cubit/account_record_cubit.dart | 16 ++ lib/account_manager/cubit/cubit.dart | 1 + lib/account_manager/models/account_info.dart | 7 +- .../account_repository.dart | 107 ++++++------- .../valid_contact_invitation.dart | 141 +++++++++++++++++ lib/layout/home.dart | 7 +- lib/layout/main_pager/account.dart | 1 + .../contact_invitation_list_manager.dart | 142 ------------------ .../lib/dht_support/src/dht_short_array.dart | 2 + .../src/dht_short_array_cubit.dart | 84 +++++++++++ 10 files changed, 295 insertions(+), 213 deletions(-) create mode 100644 lib/account_manager/cubit/account_record_cubit.dart create mode 100644 lib/contact_invitation/valid_contact_invitation.dart create mode 100644 packages/veilid_support/lib/dht_support/src/dht_short_array_cubit.dart diff --git a/lib/account_manager/cubit/account_record_cubit.dart b/lib/account_manager/cubit/account_record_cubit.dart new file mode 100644 index 0000000..65306dd --- /dev/null +++ b/lib/account_manager/cubit/account_record_cubit.dart @@ -0,0 +1,16 @@ +import 'dart:async'; + +import 'package:veilid_support/veilid_support.dart'; + +import '../../proto/proto.dart' as proto; + +class AccountRecordCubit extends DefaultDHTRecordCubit { + AccountRecordCubit({ + required super.record, + }) : super(decodeState: proto.Account.fromBuffer); + + @override + Future close() async { + await super.close(); + } +} diff --git a/lib/account_manager/cubit/cubit.dart b/lib/account_manager/cubit/cubit.dart index 0f56c84..d86274b 100644 --- a/lib/account_manager/cubit/cubit.dart +++ b/lib/account_manager/cubit/cubit.dart @@ -1,3 +1,4 @@ +export 'account_record_cubit.dart'; export 'active_user_login_cubit/active_user_login_cubit.dart'; export 'local_accounts_cubit/local_accounts_cubit.dart'; export 'user_logins_cubit/user_logins_cubit.dart'; diff --git a/lib/account_manager/models/account_info.dart b/lib/account_manager/models/account_info.dart index 14b25dc..7f2e058 100644 --- a/lib/account_manager/models/account_info.dart +++ b/lib/account_manager/models/account_info.dart @@ -1,5 +1,6 @@ import 'package:meta/meta.dart'; -import 'package:veilid_support/veilid_support.dart'; + +import 'active_account_info.dart'; enum AccountInfoStatus { noAccount, @@ -13,10 +14,10 @@ class AccountInfo { const AccountInfo({ required this.status, required this.active, - this.accountRecord, + required this.activeAccountInfo, }); final AccountInfoStatus status; final bool active; - final DHTRecord? accountRecord; + final ActiveAccountInfo? activeAccountInfo; } diff --git a/lib/account_manager/repository/account_repository/account_repository.dart b/lib/account_manager/repository/account_repository/account_repository.dart index 3132923..60d493e 100644 --- a/lib/account_manager/repository/account_repository/account_repository.dart +++ b/lib/account_manager/repository/account_repository/account_repository.dart @@ -82,66 +82,40 @@ class AccountRepository { return userLogins[idx]; } - AccountInfo getAccountInfo({required TypedKey accountMasterRecordKey}) { + AccountInfo? getAccountInfo({TypedKey? accountMasterRecordKey}) { + // Get active user if we have one + if (accountMasterRecordKey == null) { + final activeUserLogin = getActiveUserLogin(); + if (activeUserLogin == null) { + // No user logged in + return null; + } + accountMasterRecordKey = activeUserLogin; + } + // Get which local account we want to fetch the profile for final localAccount = fetchLocalAccount(accountMasterRecordKey: accountMasterRecordKey); if (localAccount == null) { // Local account does not exist return const AccountInfo( - status: AccountInfoStatus.noAccount, active: false); + status: AccountInfoStatus.noAccount, + active: false, + activeAccountInfo: null); } // See if we've logged into this account or if it is locked final activeUserLogin = getActiveUserLogin(); final active = activeUserLogin == accountMasterRecordKey; - final login = + final userLogin = fetchUserLogin(accountMasterRecordKey: accountMasterRecordKey); - if (login == null) { - // Account was locked - return AccountInfo( - status: AccountInfoStatus.accountLocked, active: active); - } - - // Pull the account DHT key, decode it and return it - final pool = DHTRecordPool.instance; - final accountRecord = - pool.getOpenedRecord(login.accountRecordInfo.accountRecord.recordKey); - if (accountRecord == null) { - // Account could not be read or decrypted from DHT - return AccountInfo( - status: AccountInfoStatus.accountInvalid, active: active); - } - - // Got account, decrypted and decoded - return AccountInfo( - status: AccountInfoStatus.accountReady, - active: active, - accountRecord: accountRecord); - } - - Future fetchActiveAccountInfo() async { - // See if we've logged into this account or if it is locked - final activeUserLogin = getActiveUserLogin(); - if (activeUserLogin == null) { - // No user logged in - return null; - } - - // Get the user login - final userLogin = fetchUserLogin(accountMasterRecordKey: activeUserLogin); if (userLogin == null) { // Account was locked - return null; - } - - // Get which local account we want to fetch the profile for - final localAccount = - fetchLocalAccount(accountMasterRecordKey: activeUserLogin); - if (localAccount == null) { - // Local account does not exist - return null; + return AccountInfo( + status: AccountInfoStatus.accountLocked, + active: active, + activeAccountInfo: null); } // Pull the account DHT key, decode it and return it @@ -149,14 +123,21 @@ class AccountRepository { final accountRecord = pool .getOpenedRecord(userLogin.accountRecordInfo.accountRecord.recordKey); if (accountRecord == null) { - return null; + // Account could not be read or decrypted from DHT + return AccountInfo( + status: AccountInfoStatus.accountInvalid, + active: active, + activeAccountInfo: null); } // Got account, decrypted and decoded - return ActiveAccountInfo( - localAccount: localAccount, - userLogin: userLogin, - accountRecord: accountRecord, + return AccountInfo( + status: AccountInfoStatus.accountReady, + active: active, + activeAccountInfo: ActiveAccountInfo( + localAccount: localAccount, + userLogin: userLogin, + accountRecord: accountRecord), ); } @@ -411,24 +392,21 @@ class AccountRepository { // For all user logins if they arent open yet final activeLogins = await _activeLogins.get(); for (final userLogin in activeLogins.userLogins) { + //// Account record key ///////////////////////////// final accountRecordKey = userLogin.accountRecordInfo.accountRecord.recordKey; final existingAccountRecord = pool.getOpenedRecord(accountRecordKey); - if (existingAccountRecord != null) { - continue; + if (existingAccountRecord == null) { + final localAccount = fetchLocalAccount( + accountMasterRecordKey: userLogin.accountMasterRecordKey); + + // Record not yet open, do it + final record = await pool.openOwned( + userLogin.accountRecordInfo.accountRecord, + parent: localAccount!.identityMaster.identityRecordKey); + // Watch the record's only (default) key + await record.watch(); } - final localAccount = fetchLocalAccount( - accountMasterRecordKey: userLogin.accountMasterRecordKey); - - // Record not yet open, do it - final record = await pool.openOwned( - userLogin.accountRecordInfo.accountRecord, - parent: localAccount!.identityMaster.identityRecordKey); - // Watch the record's only (default) key - await record.watch(); - - // .scope( - // (accountRec) => accountRec.getProtobuf(proto.Account.fromBuffer)); } } @@ -437,6 +415,7 @@ class AccountRepository { final activeLogins = await _activeLogins.get(); for (final userLogin in activeLogins.userLogins) { + //// Account record key ///////////////////////////// final accountRecordKey = userLogin.accountRecordInfo.accountRecord.recordKey; final accountRecord = pool.getOpenedRecord(accountRecordKey); diff --git a/lib/contact_invitation/valid_contact_invitation.dart b/lib/contact_invitation/valid_contact_invitation.dart new file mode 100644 index 0000000..ec21444 --- /dev/null +++ b/lib/contact_invitation/valid_contact_invitation.dart @@ -0,0 +1,141 @@ +////////////////////////////////////////////////// +/// + +class ValidContactInvitation { + ValidContactInvitation._( + {required ContactInvitationListManager contactInvitationManager, + required proto.SignedContactInvitation signedContactInvitation, + required proto.ContactInvitation contactInvitation, + required TypedKey contactRequestInboxKey, + required proto.ContactRequest contactRequest, + required proto.ContactRequestPrivate contactRequestPrivate, + required IdentityMaster contactIdentityMaster, + required KeyPair writer}) + : _contactInvitationManager = contactInvitationManager, + _signedContactInvitation = signedContactInvitation, + _contactInvitation = contactInvitation, + _contactRequestInboxKey = contactRequestInboxKey, + _contactRequest = contactRequest, + _contactRequestPrivate = contactRequestPrivate, + _contactIdentityMaster = contactIdentityMaster, + _writer = writer; + + Future accept() async { + final pool = await DHTRecordPool.instance(); + final activeAccountInfo = _contactInvitationManager._activeAccountInfo; + try { + // Ensure we don't delete this if we're trying to chat to self + final isSelf = _contactIdentityMaster.identityPublicKey == + activeAccountInfo.localAccount.identityMaster.identityPublicKey; + final accountRecordKey = + activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey; + + return (await pool.openWrite(_contactRequestInboxKey, _writer, + parent: accountRecordKey)) + // ignore: prefer_expression_function_bodies + .maybeDeleteScope(!isSelf, (contactRequestInbox) async { + // Create local conversation key for this + // contact and send via contact response + return createConversation( + activeAccountInfo: activeAccountInfo, + remoteIdentityPublicKey: + _contactIdentityMaster.identityPublicTypedKey(), + callback: (localConversation) async { + final contactResponse = proto.ContactResponse() + ..accept = true + ..remoteConversationRecordKey = localConversation.key.toProto() + ..identityMasterRecordKey = activeAccountInfo + .localAccount.identityMaster.masterRecordKey + .toProto(); + final contactResponseBytes = contactResponse.writeToBuffer(); + + final cs = await pool.veilid + .getCryptoSystem(_contactRequestInboxKey.kind); + + final identitySignature = await cs.sign( + activeAccountInfo + .localAccount.identityMaster.identityPublicKey, + activeAccountInfo.userLogin.identitySecret.value, + contactResponseBytes); + + final signedContactResponse = proto.SignedContactResponse() + ..contactResponse = contactResponseBytes + ..identitySignature = identitySignature.toProto(); + + // Write the acceptance to the inbox + if (await contactRequestInbox.tryWriteProtobuf( + proto.SignedContactResponse.fromBuffer, + signedContactResponse, + subkey: 1) != + null) { + throw Exception('failed to accept contact invitation'); + } + return AcceptedContact( + profile: _contactRequestPrivate.profile, + remoteIdentity: _contactIdentityMaster, + remoteConversationRecordKey: proto.TypedKeyProto.fromProto( + _contactRequestPrivate.chatRecordKey), + localConversationRecordKey: localConversation.key, + ); + }); + }); + } on Exception catch (e) { + log.debug('exception: $e', e); + return null; + } + } + + Future reject() async { + final pool = await DHTRecordPool.instance(); + final activeAccountInfo = _contactInvitationManager._activeAccountInfo; + + // Ensure we don't delete this if we're trying to chat to self + final isSelf = _contactIdentityMaster.identityPublicKey == + activeAccountInfo.localAccount.identityMaster.identityPublicKey; + final accountRecordKey = + activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey; + + return (await pool.openWrite(_contactRequestInboxKey, _writer, + parent: accountRecordKey)) + .maybeDeleteScope(!isSelf, (contactRequestInbox) async { + final cs = + await pool.veilid.getCryptoSystem(_contactRequestInboxKey.kind); + + final contactResponse = proto.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 = proto.SignedContactResponse() + ..contactResponse = contactResponseBytes + ..identitySignature = identitySignature.toProto(); + + // Write the rejection to the inbox + if (await contactRequestInbox.tryWriteProtobuf( + proto.SignedContactResponse.fromBuffer, signedContactResponse, + subkey: 1) != + null) { + log.error('failed to reject contact invitation'); + return false; + } + return true; + }); + } + + // + ContactInvitationListManager _contactInvitationManager; + proto.SignedContactInvitation _signedContactInvitation; + proto.ContactInvitation _contactInvitation; + TypedKey _contactRequestInboxKey; + proto.ContactRequest _contactRequest; + proto.ContactRequestPrivate _contactRequestPrivate; + IdentityMaster _contactIdentityMaster; + KeyPair _writer; +} diff --git a/lib/layout/home.dart b/lib/layout/home.dart index eeec9dc..438ec6a 100644 --- a/lib/layout/home.dart +++ b/lib/layout/home.dart @@ -67,8 +67,7 @@ class HomePageState extends State with TickerProviderStateMixin { final scale = theme.extension()!; return BlocProvider( - create: (context) => DefaultDHTRecordCubit( - record: accountRecord, decodeState: proto.Account.fromBuffer), + create: (context) => AccountRecordCubit(record: accountRecord), child: Column(children: [ Row(children: [ IconButton( @@ -87,7 +86,7 @@ class HomePageState extends State with TickerProviderStateMixin { context.go('/home/settings'); }).paddingLTRB(0, 0, 8, 0), context - .watch>() + .watch() .state .builder((context, account) => ProfileWidget( name: account.profile.name, @@ -96,7 +95,7 @@ class HomePageState extends State with TickerProviderStateMixin { .expanded(), ]).paddingAll(8), context - .watch>() + .watch() .state .builder((context, account) => MainPager( localAccounts: localAccounts, diff --git a/lib/layout/main_pager/account.dart b/lib/layout/main_pager/account.dart index ece96c1..3f25171 100644 --- a/lib/layout/main_pager/account.dart +++ b/lib/layout/main_pager/account.dart @@ -11,6 +11,7 @@ import '../../../proto/proto.dart' as proto; import '../../account_manager/account_manager.dart'; import '../../contact_invitation/contact_invitation.dart'; import '../../contacts/contacts.dart'; +import '../../theme/theme.dart'; class AccountPage extends StatefulWidget { const AccountPage({ diff --git a/lib/old_to_refactor/providers/contact_invitation_list_manager.dart b/lib/old_to_refactor/providers/contact_invitation_list_manager.dart index 550169e..04cb273 100644 --- a/lib/old_to_refactor/providers/contact_invitation_list_manager.dart +++ b/lib/old_to_refactor/providers/contact_invitation_list_manager.dart @@ -439,145 +439,3 @@ class ContactInvitationListManager extends _$ContactInvitationListManager { final DHTShortArray _dhtRecord; IList _records; } - -////////////////////////////////////////////////// -/// - -class ValidContactInvitation { - ValidContactInvitation._( - {required ContactInvitationListManager contactInvitationManager, - required proto.SignedContactInvitation signedContactInvitation, - required proto.ContactInvitation contactInvitation, - required TypedKey contactRequestInboxKey, - required proto.ContactRequest contactRequest, - required proto.ContactRequestPrivate contactRequestPrivate, - required IdentityMaster contactIdentityMaster, - required KeyPair writer}) - : _contactInvitationManager = contactInvitationManager, - _signedContactInvitation = signedContactInvitation, - _contactInvitation = contactInvitation, - _contactRequestInboxKey = contactRequestInboxKey, - _contactRequest = contactRequest, - _contactRequestPrivate = contactRequestPrivate, - _contactIdentityMaster = contactIdentityMaster, - _writer = writer; - - Future accept() async { - final pool = await DHTRecordPool.instance(); - final activeAccountInfo = _contactInvitationManager._activeAccountInfo; - try { - // Ensure we don't delete this if we're trying to chat to self - final isSelf = _contactIdentityMaster.identityPublicKey == - activeAccountInfo.localAccount.identityMaster.identityPublicKey; - final accountRecordKey = - activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey; - - return (await pool.openWrite(_contactRequestInboxKey, _writer, - parent: accountRecordKey)) - // ignore: prefer_expression_function_bodies - .maybeDeleteScope(!isSelf, (contactRequestInbox) async { - // Create local conversation key for this - // contact and send via contact response - return createConversation( - activeAccountInfo: activeAccountInfo, - remoteIdentityPublicKey: - _contactIdentityMaster.identityPublicTypedKey(), - callback: (localConversation) async { - final contactResponse = proto.ContactResponse() - ..accept = true - ..remoteConversationRecordKey = localConversation.key.toProto() - ..identityMasterRecordKey = activeAccountInfo - .localAccount.identityMaster.masterRecordKey - .toProto(); - final contactResponseBytes = contactResponse.writeToBuffer(); - - final cs = await pool.veilid - .getCryptoSystem(_contactRequestInboxKey.kind); - - final identitySignature = await cs.sign( - activeAccountInfo - .localAccount.identityMaster.identityPublicKey, - activeAccountInfo.userLogin.identitySecret.value, - contactResponseBytes); - - final signedContactResponse = proto.SignedContactResponse() - ..contactResponse = contactResponseBytes - ..identitySignature = identitySignature.toProto(); - - // Write the acceptance to the inbox - if (await contactRequestInbox.tryWriteProtobuf( - proto.SignedContactResponse.fromBuffer, - signedContactResponse, - subkey: 1) != - null) { - throw Exception('failed to accept contact invitation'); - } - return AcceptedContact( - profile: _contactRequestPrivate.profile, - remoteIdentity: _contactIdentityMaster, - remoteConversationRecordKey: proto.TypedKeyProto.fromProto( - _contactRequestPrivate.chatRecordKey), - localConversationRecordKey: localConversation.key, - ); - }); - }); - } on Exception catch (e) { - log.debug('exception: $e', e); - return null; - } - } - - Future reject() async { - final pool = await DHTRecordPool.instance(); - final activeAccountInfo = _contactInvitationManager._activeAccountInfo; - - // Ensure we don't delete this if we're trying to chat to self - final isSelf = _contactIdentityMaster.identityPublicKey == - activeAccountInfo.localAccount.identityMaster.identityPublicKey; - final accountRecordKey = - activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey; - - return (await pool.openWrite(_contactRequestInboxKey, _writer, - parent: accountRecordKey)) - .maybeDeleteScope(!isSelf, (contactRequestInbox) async { - final cs = - await pool.veilid.getCryptoSystem(_contactRequestInboxKey.kind); - - final contactResponse = proto.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 = proto.SignedContactResponse() - ..contactResponse = contactResponseBytes - ..identitySignature = identitySignature.toProto(); - - // Write the rejection to the inbox - if (await contactRequestInbox.tryWriteProtobuf( - proto.SignedContactResponse.fromBuffer, signedContactResponse, - subkey: 1) != - null) { - log.error('failed to reject contact invitation'); - return false; - } - return true; - }); - } - - // - ContactInvitationListManager _contactInvitationManager; - proto.SignedContactInvitation _signedContactInvitation; - proto.ContactInvitation _contactInvitation; - TypedKey _contactRequestInboxKey; - proto.ContactRequest _contactRequest; - proto.ContactRequestPrivate _contactRequestPrivate; - IdentityMaster _contactIdentityMaster; - KeyPair _writer; -} diff --git a/packages/veilid_support/lib/dht_support/src/dht_short_array.dart b/packages/veilid_support/lib/dht_support/src/dht_short_array.dart index ed917d1..08e2e95 100644 --- a/packages/veilid_support/lib/dht_support/src/dht_short_array.dart +++ b/packages/veilid_support/lib/dht_support/src/dht_short_array.dart @@ -29,6 +29,8 @@ class _DHTShortArrayCache { } } +xxxx add listening to head and linked records + class DHTShortArray { DHTShortArray._({required DHTRecord headRecord}) : _headRecord = headRecord, diff --git a/packages/veilid_support/lib/dht_support/src/dht_short_array_cubit.dart b/packages/veilid_support/lib/dht_support/src/dht_short_array_cubit.dart new file mode 100644 index 0000000..3484b0f --- /dev/null +++ b/packages/veilid_support/lib/dht_support/src/dht_short_array_cubit.dart @@ -0,0 +1,84 @@ +import 'dart:async'; +import 'dart:typed_data'; + +import 'package:bloc/bloc.dart'; +import 'package:fast_immutable_collections/fast_immutable_collections.dart'; + +import '../../veilid_support.dart'; + +class DHTShortArrayCubit extends Cubit>> { + DHTShortArrayCubit({ + required DHTShortArray shortArray, + required T Function(List data) decodeElement, + }) : super(const AsyncValue.loading()) { + Future.delayed(Duration.zero, () async { + // Make initial state update + try { + final initialState = await initialStateFunction(record); + if (initialState != null) { + emit(AsyncValue.data(initialState)); + } + } on Exception catch (e) { + emit(AsyncValue.error(e)); + } + + shortArray. xxx add listen to head and linked records in dht_short_array + + _subscription = await record.listen((update) async { + try { + final newState = + await stateFunction(record, update.subkeys, update.valueData); + if (newState != null) { + emit(AsyncValue.data(newState)); + } + } on Exception catch (e) { + emit(AsyncValue.error(e)); + } + }); + }); + } + + @override + Future close() async { + await _subscription?.cancel(); + _subscription = null; + await super.close(); + } + + StreamSubscription? _subscription; +} + +// Cubit that watches the default subkey value of a dhtrecord +class DefaultDHTRecordCubit extends DHTRecordCubit { + DefaultDHTRecordCubit({ + required super.record, + required T Function(List data) decodeState, + }) : super( + initialStateFunction: (record) async { + final initialData = await record.get(); + if (initialData == null) { + return null; + } + return decodeState(initialData); + }, + stateFunction: (record, subkeys, valueData) async { + final defaultSubkey = record.subkeyOrDefault(-1); + if (subkeys.containsSubkey(defaultSubkey)) { + final Uint8List data; + final firstSubkey = subkeys.firstOrNull!.low; + if (firstSubkey != defaultSubkey) { + final maybeData = await record.get(forceRefresh: true); + if (maybeData == null) { + return null; + } + data = maybeData; + } else { + data = valueData.data; + } + final newState = decodeState(data); + return newState; + } + return null; + }, + ); +} From b99e387dac78d447e473d0985f341e3267ed5c15 Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Fri, 19 Jan 2024 14:33:30 -0500 Subject: [PATCH 12/68] xfer --- .../lib/dht_support/src/dht_short_array.dart | 31 +++++++++++++++++-- 1 file changed, 29 insertions(+), 2 deletions(-) diff --git a/packages/veilid_support/lib/dht_support/src/dht_short_array.dart b/packages/veilid_support/lib/dht_support/src/dht_short_array.dart index 08e2e95..88aff94 100644 --- a/packages/veilid_support/lib/dht_support/src/dht_short_array.dart +++ b/packages/veilid_support/lib/dht_support/src/dht_short_array.dart @@ -29,8 +29,6 @@ class _DHTShortArrayCache { } } -xxxx add listening to head and linked records - class DHTShortArray { DHTShortArray._({required DHTRecord headRecord}) : _headRecord = headRecord, @@ -610,4 +608,33 @@ class DHTShortArray { Future Function(T) update, ) => eventualUpdateItem(pos, protobufUpdate(fromBuffer, update)); + + Future watch() async { + // Watch head and all linked records + try { + await [_headRecord.watch(), ..._head.linkedRecords.map((r) => r.watch())] + .wait; + } finally { + await [ + _headRecord.cancelWatch(), + ..._head.linkedRecords.map((r) => r.cancelWatch()) + ].wait; + } + } + + Future listen( + Future Function() onChanged, + ) async { + _headRecord.listen((update) => { + xxx + } + } + + Future cancelWatch() async { + // Watch head and all linked records + await _headRecord.cancelWatch(); + for (final lr in _head.linkedRecords) { + await lr.cancelWatch(); + } + } } From 1534a77ab19df70e50d7ae9cca6338f91f5239d5 Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Wed, 24 Jan 2024 20:59:39 -0500 Subject: [PATCH 13/68] watch for dhtshortarray --- .../lib/dht_support/src/dht_record.dart | 22 ++- .../lib/dht_support/src/dht_record_pool.dart | 31 +++- .../lib/dht_support/src/dht_short_array.dart | 172 +++++++++++++++--- .../src/dht_short_array_cubit.dart | 2 +- packages/veilid_support/pubspec.lock | 4 +- packages/veilid_support/pubspec.yaml | 2 + 6 files changed, 190 insertions(+), 43 deletions(-) diff --git a/packages/veilid_support/lib/dht_support/src/dht_record.dart b/packages/veilid_support/lib/dht_support/src/dht_record.dart index 1d994b3..b6f1b6d 100644 --- a/packages/veilid_support/lib/dht_support/src/dht_record.dart +++ b/packages/veilid_support/lib/dht_support/src/dht_record.dart @@ -1,6 +1,8 @@ import 'dart:async'; import 'dart:typed_data'; +import 'package:fast_immutable_collections/fast_immutable_collections.dart'; +import 'package:meta/meta.dart'; import 'package:protobuf/protobuf.dart'; import '../../../veilid_support.dart'; @@ -31,9 +33,13 @@ class DHTRecord { final DHTRecordCrypto _crypto; bool _open; bool _valid; + @internal StreamController? watchController; + @internal bool needsWatchStateUpdate; + @internal bool inWatchStateUpdate; + @internal WatchState? watchState; int subkeyOrDefault(int subkey) => (subkey == -1) ? _defaultSubkey : subkey; @@ -45,6 +51,7 @@ class DHTRecord { DHTSchema get schema => _recordDescriptor.schema; int get subkeyCount => _recordDescriptor.schema.subkeyCount(); KeyPair? get writer => _writer; + DHTRecordCrypto get crypto => _crypto; OwnedDHTRecordPointer get ownedDHTRecordPointer => OwnedDHTRecordPointer(recordKey: key, owner: ownerKeyPair!); @@ -266,9 +273,12 @@ class DHTRecord { Timestamp? expiration, int? count}) async { // Set up watch requirements which will get picked up by the next tick - watchState = - WatchState(subkeys: subkeys, expiration: expiration, count: count); - needsWatchStateUpdate = true; + final oldWatchState = watchState; + watchState = WatchState( + subkeys: subkeys?.lock, expiration: expiration, count: count); + if (oldWatchState != watchState) { + needsWatchStateUpdate = true; + } } Future> listen( @@ -294,7 +304,9 @@ class DHTRecord { Future cancelWatch() async { // Tear down watch requirements - watchState = null; - needsWatchStateUpdate = true; + if (watchState != null) { + watchState = null; + needsWatchStateUpdate = true; + } } } diff --git a/packages/veilid_support/lib/dht_support/src/dht_record_pool.dart b/packages/veilid_support/lib/dht_support/src/dht_record_pool.dart index 465310b..1c09d45 100644 --- a/packages/veilid_support/lib/dht_support/src/dht_record_pool.dart +++ b/packages/veilid_support/lib/dht_support/src/dht_record_pool.dart @@ -1,5 +1,6 @@ import 'dart:async'; +import 'package:equatable/equatable.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; @@ -37,13 +38,19 @@ class OwnedDHTRecordPointer with _$OwnedDHTRecordPointer { } /// Watch state -class WatchState { - WatchState( - {required this.subkeys, required this.expiration, required this.count}); - List? subkeys; - Timestamp? expiration; - int? count; - Timestamp? realExpiration; +class WatchState extends Equatable { + const WatchState( + {required this.subkeys, + required this.expiration, + required this.count, + this.realExpiration}); + final IList? subkeys; + final Timestamp? expiration; + final int? count; + final Timestamp? realExpiration; + + @override + List get props => [subkeys, expiration, count, realExpiration]; } class DHTRecordPool with TableDBBacked { @@ -400,11 +407,17 @@ class DHTRecordPool with TableDBBacked { try { final realExpiration = await kv.value.routingContext .watchDHTValues(kv.key, - subkeys: ws.subkeys, + subkeys: ws.subkeys?.toList(), count: ws.count, expiration: ws.expiration); kv.value.needsWatchStateUpdate = false; - ws.realExpiration = realExpiration; + + // Update watch state with real expiration + kv.value.watchState = WatchState( + subkeys: ws.subkeys, + expiration: ws.expiration, + count: ws.count, + realExpiration: realExpiration); } on VeilidAPIException { // Failed to cancel DHT watch, try again next tick } diff --git a/packages/veilid_support/lib/dht_support/src/dht_short_array.dart b/packages/veilid_support/lib/dht_support/src/dht_short_array.dart index 88aff94..136520c 100644 --- a/packages/veilid_support/lib/dht_support/src/dht_short_array.dart +++ b/packages/veilid_support/lib/dht_support/src/dht_short_array.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'dart:typed_data'; +import 'package:mutex/mutex.dart'; import 'package:protobuf/protobuf.dart'; import '../../../../veilid_support.dart'; @@ -32,7 +33,9 @@ class _DHTShortArrayCache { class DHTShortArray { DHTShortArray._({required DHTRecord headRecord}) : _headRecord = headRecord, - _head = _DHTShortArrayCache() { + _head = _DHTShortArrayCache(), + _subscriptions = {}, + _listenMutex = Mutex() { late final int stride; switch (headRecord.schema) { case DHTSchemaDFLT(oCnt: final oCnt): @@ -59,6 +62,14 @@ class DHTShortArray { // Cached representation refreshed from head record _DHTShortArrayCache _head; + // Subscription to head and linked record internal changes + final Map> + _subscriptions; + // Stream of external changes + StreamController? _watchController; + // Watch mutex to ensure we keep the representation valid + final Mutex _listenMutex; + // Create a DHTShortArray // if smplWriter is specified, uses a SMPL schema with a single writer // rather than the key owner @@ -273,6 +284,11 @@ class DHTShortArray { linkedKeys.map((key) => (sameRecords[key] ?? newRecords[key])!)) ..index.addAll(index) ..free.addAll(free); + + // Update watch if we have one in case linked records have been added + if (_watchController != null) { + await _watchAllRecords(); + } } /// Pull the latest or updated copy of the head record from the network @@ -280,7 +296,7 @@ class DHTShortArray { {bool forceRefresh = true, bool onlyUpdates = false}) async { // Get an updated head record copy if one exists final head = await _headRecord.getProtobuf(proto.DHTShortArray.fromBuffer, - forceRefresh: forceRefresh, onlyUpdates: onlyUpdates); + subkey: 0, forceRefresh: forceRefresh, onlyUpdates: onlyUpdates); if (head == null) { if (onlyUpdates) { // No update @@ -297,6 +313,7 @@ class DHTShortArray { //////////////////////////////////////////////////////////////// Future close() async { + await _watchController?.close(); final futures = >[_headRecord.close()]; for (final lr in _head.linkedRecords) { futures.add(lr.close()); @@ -305,7 +322,9 @@ class DHTShortArray { } Future delete() async { - final futures = >[_headRecord.close()]; + await _watchController?.close(); + + final futures = >[_headRecord.delete()]; for (final lr in _head.linkedRecords) { futures.add(lr.delete()); } @@ -332,7 +351,7 @@ class DHTShortArray { } } - DHTRecord? _getRecord(int recordNumber) { + DHTRecord? _getLinkedRecord(int recordNumber) { if (recordNumber == 0) { return _headRecord; } @@ -343,6 +362,43 @@ class DHTShortArray { return _head.linkedRecords[recordNumber]; } + Future _getOrCreateLinkedRecord(int recordNumber) async { + if (recordNumber == 0) { + return _headRecord; + } + final pool = DHTRecordPool.instance; + recordNumber--; + while (recordNumber >= _head.linkedRecords.length) { + // Linked records must use SMPL schema so writer can be specified + // Use the same writer as the head record + final smplWriter = _headRecord.writer!; + final parent = pool.getParentRecordKey(_headRecord.key); + final routingContext = _headRecord.routingContext; + final crypto = _headRecord.crypto; + + final schema = DHTSchema.smpl( + oCnt: 0, + members: [DHTSchemaMember(mKey: smplWriter.key, mCnt: _stride)]); + final dhtCreateRecord = await pool.create( + parent: parent, + routingContext: routingContext, + schema: schema, + crypto: crypto, + writer: smplWriter); + // Reopen with SMPL writer + await dhtCreateRecord.close(); + final dhtRecord = await pool.openWrite(dhtCreateRecord.key, smplWriter, + parent: parent, routingContext: routingContext, crypto: crypto); + + // Add to linked records + _head.linkedRecords.add(dhtRecord); + if (!await _tryWriteHead()) { + await _refreshHead(); + } + } + return _head.linkedRecords[recordNumber]; + } + int _emptyIndex() { if (_head.free.isNotEmpty) { return _head.free.removeLast(); @@ -368,7 +424,7 @@ class DHTShortArray { } final index = _head.index[pos]; final recordNumber = index ~/ _stride; - final record = _getRecord(recordNumber); + final record = _getLinkedRecord(recordNumber); assert(record != null, 'Record does not exist'); final recordSubkey = (index % _stride) + ((recordNumber == 0) ? 1 : 0); @@ -472,7 +528,7 @@ class DHTShortArray { final removedIdx = _head.index.removeAt(pos); _freeIndex(removedIdx); final recordNumber = removedIdx ~/ _stride; - final record = _getRecord(recordNumber); + final record = _getLinkedRecord(recordNumber); assert(record != null, 'Record does not exist'); final recordSubkey = (removedIdx % _stride) + ((recordNumber == 0) ? 1 : 0); @@ -532,11 +588,10 @@ class DHTShortArray { final index = _head.index[pos]; final recordNumber = index ~/ _stride; - final record = _getRecord(recordNumber); - assert(record != null, 'Record does not exist'); + final record = await _getOrCreateLinkedRecord(recordNumber); final recordSubkey = (index % _stride) + ((recordNumber == 0) ? 1 : 0); - return record!.tryWriteBytes(newValue, subkey: recordSubkey); + return record.tryWriteBytes(newValue, subkey: recordSubkey); } Future eventualWriteItem(int pos, Uint8List newValue) async { @@ -609,32 +664,97 @@ class DHTShortArray { ) => eventualUpdateItem(pos, protobufUpdate(fromBuffer, update)); - Future watch() async { - // Watch head and all linked records + // Watch head and all linked records + Future _watchAllRecords() async { + // This will update any existing watches if necessary try { await [_headRecord.watch(), ..._head.linkedRecords.map((r) => r.watch())] .wait; - } finally { - await [ - _headRecord.cancelWatch(), - ..._head.linkedRecords.map((r) => r.cancelWatch()) - ].wait; + + // Update changes to the head record + if (!_subscriptions.containsKey(_headRecord.key)) { + _subscriptions[_headRecord.key] = + await _headRecord.listen(_onUpdateRecord); + } + // Update changes to any linked records + for (final lr in _head.linkedRecords) { + if (!_subscriptions.containsKey(lr.key)) { + _subscriptions[lr.key] = await lr.listen(_onUpdateRecord); + } + } + } on Exception { + // If anything fails, try to cancel the watches + await _cancelRecordWatches(); + rethrow; } } - Future listen( - Future Function() onChanged, - ) async { - _headRecord.listen((update) => { - xxx - } - } - - Future cancelWatch() async { - // Watch head and all linked records + // Stop watching for changes to head and linked records + Future _cancelRecordWatches() async { await _headRecord.cancelWatch(); for (final lr in _head.linkedRecords) { await lr.cancelWatch(); } + await _subscriptions.values.map((s) => s.cancel()).wait; + _subscriptions.clear(); } + + // Called when a head or linked record changes + Future _onUpdateRecord(VeilidUpdateValueChange update) async { + final record = _head.linkedRecords.firstWhere( + (element) => element.key == update.key, + orElse: () => _headRecord); + + // If head record subkey zero changes, then the layout + // of the dhtshortarray has changed + var updateHead = false; + if (record == _headRecord && update.subkeys.containsSubkey(0)) { + updateHead = true; + } + + // If we have any other subkeys to update, do them first + final unord = List>.empty(growable: true); + for (final skr in update.subkeys) { + for (var subkey = skr.low; subkey <= skr.high; subkey++) { + // Skip head subkey + if (subkey == 0) { + continue; + } + // Get the subkey, which caches the result in the local record store + unord.add(record.get(subkey: subkey, forceRefresh: true)); + } + } + await unord.wait; + + // Then update the head record + if (updateHead) { + await _refreshHead(forceRefresh: false); + } + // Then commit the change to any listeners + _watchController?.sink.add(null); + } + + Future> listen( + void Function() onChanged, + ) => + _listenMutex.protect(() async { + // If don't have a controller yet, set it up + if (_watchController == null) { + // Set up watch requirements + _watchController = StreamController.broadcast(onCancel: () { + // If there are no more listeners then we can get + // rid of the controller and drop our subscriptions + unawaited(_listenMutex.protect(() async { + // Cancel watches of head and linked records + await _cancelRecordWatches(); + _watchController = null; + })); + }); + + // Start watching head and linked records + await _watchAllRecords(); + } + // Return subscription + return _watchController!.stream.listen((_) => onChanged()); + }); } diff --git a/packages/veilid_support/lib/dht_support/src/dht_short_array_cubit.dart b/packages/veilid_support/lib/dht_support/src/dht_short_array_cubit.dart index 3484b0f..c1f1bd8 100644 --- a/packages/veilid_support/lib/dht_support/src/dht_short_array_cubit.dart +++ b/packages/veilid_support/lib/dht_support/src/dht_short_array_cubit.dart @@ -21,7 +21,7 @@ class DHTShortArrayCubit extends Cubit>> { } on Exception catch (e) { emit(AsyncValue.error(e)); } - +xxx do this now shortArray. xxx add listen to head and linked records in dht_short_array _subscription = await record.listen((update) async { diff --git a/packages/veilid_support/pubspec.lock b/packages/veilid_support/pubspec.lock index d80f0e7..ec62d29 100644 --- a/packages/veilid_support/pubspec.lock +++ b/packages/veilid_support/pubspec.lock @@ -194,7 +194,7 @@ packages: source: hosted version: "2.3.4" equatable: - dependency: transitive + dependency: "direct main" description: name: equatable sha256: c2b87cb7756efdf69892005af546c56c0b5037f54d2a88269b4f347a505e3ca2 @@ -388,7 +388,7 @@ packages: source: hosted version: "0.5.0" meta: - dependency: transitive + dependency: "direct main" description: name: meta sha256: a6e590c838b18133bb482a2745ad77c5bb7715fb0451209e1a7567d416678b8e diff --git a/packages/veilid_support/pubspec.yaml b/packages/veilid_support/pubspec.yaml index 44e6e08..471d00b 100644 --- a/packages/veilid_support/pubspec.yaml +++ b/packages/veilid_support/pubspec.yaml @@ -8,10 +8,12 @@ environment: dependencies: bloc: ^8.1.2 + equatable: ^2.0.5 fast_immutable_collections: ^9.1.5 freezed_annotation: ^2.2.0 json_annotation: ^4.8.1 loggy: ^2.0.3 + meta: ^1.10.0 mutex: ^3.1.0 protobuf: ^3.0.0 veilid: From 0291ff72247ebd86b000c5ba226c8bf175f1fae8 Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Thu, 25 Jan 2024 09:30:02 -0500 Subject: [PATCH 14/68] short array cubit --- .../src/dht_short_array_cubit.dart | 104 ++++++++---------- 1 file changed, 48 insertions(+), 56 deletions(-) diff --git a/packages/veilid_support/lib/dht_support/src/dht_short_array_cubit.dart b/packages/veilid_support/lib/dht_support/src/dht_short_array_cubit.dart index c1f1bd8..735d79a 100644 --- a/packages/veilid_support/lib/dht_support/src/dht_short_array_cubit.dart +++ b/packages/veilid_support/lib/dht_support/src/dht_short_array_cubit.dart @@ -1,5 +1,4 @@ import 'dart:async'; -import 'dart:typed_data'; import 'package:bloc/bloc.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; @@ -10,34 +9,58 @@ class DHTShortArrayCubit extends Cubit>> { DHTShortArrayCubit({ required DHTShortArray shortArray, required T Function(List data) decodeElement, - }) : super(const AsyncValue.loading()) { + }) : _shortArray = shortArray, + _decodeElement = decodeElement, + _wantsUpdate = false, + _isUpdating = false, + super(const AsyncValue.loading()) { + // Make initial state update + _update(); Future.delayed(Duration.zero, () async { - // Make initial state update - try { - final initialState = await initialStateFunction(record); - if (initialState != null) { - emit(AsyncValue.data(initialState)); - } - } on Exception catch (e) { - emit(AsyncValue.error(e)); - } -xxx do this now - shortArray. xxx add listen to head and linked records in dht_short_array + _subscription = await shortArray.listen(_update); + }); + } - _subscription = await record.listen((update) async { + void _update() { + // Run at most one background update process + _wantsUpdate = true; + if (_isUpdating) { + return; + } + _isUpdating = true; + Future.delayed(Duration.zero, () async { + // Keep updating until we don't want to update any more + // Because this is async, we could get an update while we're + // still processing the last one + do { + _wantsUpdate = false; try { - final newState = - await stateFunction(record, update.subkeys, update.valueData); - if (newState != null) { - emit(AsyncValue.data(newState)); - } + final initialState = await _getElements(); + emit(AsyncValue.data(initialState)); } on Exception catch (e) { emit(AsyncValue.error(e)); } - }); + } while (_wantsUpdate); + + // Note that this update future has finished + _isUpdating = false; }); } + // Get and decode the entire short array + Future> _getElements() async { + var out = IList(); + for (var i = 0; i < _shortArray.length; i++) { + // Get the element bytes (throw if fails, array state is invalid) + final bytes = (await _shortArray.getItem(i))!; + // Decode the element + final elem = _decodeElement(bytes); + // Append to the output list + out = out.add(elem); + } + return out; + } + @override Future close() async { await _subscription?.cancel(); @@ -45,40 +68,9 @@ xxx do this now await super.close(); } - StreamSubscription? _subscription; -} - -// Cubit that watches the default subkey value of a dhtrecord -class DefaultDHTRecordCubit extends DHTRecordCubit { - DefaultDHTRecordCubit({ - required super.record, - required T Function(List data) decodeState, - }) : super( - initialStateFunction: (record) async { - final initialData = await record.get(); - if (initialData == null) { - return null; - } - return decodeState(initialData); - }, - stateFunction: (record, subkeys, valueData) async { - final defaultSubkey = record.subkeyOrDefault(-1); - if (subkeys.containsSubkey(defaultSubkey)) { - final Uint8List data; - final firstSubkey = subkeys.firstOrNull!.low; - if (firstSubkey != defaultSubkey) { - final maybeData = await record.get(forceRefresh: true); - if (maybeData == null) { - return null; - } - data = maybeData; - } else { - data = valueData.data; - } - final newState = decodeState(data); - return newState; - } - return null; - }, - ); + final DHTShortArray _shortArray; + final T Function(List data) _decodeElement; + StreamSubscription? _subscription; + bool _wantsUpdate; + bool _isUpdating; } From b35b618a4d33e285de4804784179c67b529b0729 Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Thu, 25 Jan 2024 20:33:56 -0500 Subject: [PATCH 15/68] more refactor --- .../models/active_account_info.dart | 5 +- .../contact_invitation.dart | 1 + .../models/accepted_contact.dart | 19 ++ lib/contact_invitation/models/models.dart | 2 + .../contact_invitation_repository.dart} | 199 +++++++----------- .../valid_contact_invitation.dart | 54 ++--- lib/layout/home.dart | 5 +- .../{account.dart => account_page.dart} | 4 +- .../{chats.dart => chats_page.dart} | 0 .../lib/dht_support/src/dht_short_array.dart | 25 ++- 10 files changed, 162 insertions(+), 152 deletions(-) create mode 100644 lib/contact_invitation/models/accepted_contact.dart create mode 100644 lib/contact_invitation/models/models.dart rename lib/{old_to_refactor/providers/contact_invitation_list_manager.dart => contact_invitation/repository/contact_invitation_repository.dart} (73%) rename lib/contact_invitation/{ => repository}/valid_contact_invitation.dart (77%) rename lib/layout/main_pager/{account.dart => account_page.dart} (98%) rename lib/layout/main_pager/{chats.dart => chats_page.dart} (100%) diff --git a/lib/account_manager/models/active_account_info.dart b/lib/account_manager/models/active_account_info.dart index b20dd6e..6a6afe0 100644 --- a/lib/account_manager/models/active_account_info.dart +++ b/lib/account_manager/models/active_account_info.dart @@ -13,7 +13,10 @@ class ActiveAccountInfo { }); // - KeyPair getConversationWriter() { + TypedKey get accountRecordKey => + userLogin.accountRecordInfo.accountRecord.recordKey; + + KeyPair get conversationWriter { final identityKey = localAccount.identityMaster.identityPublicKey; final identitySecret = userLogin.identitySecret; return KeyPair(key: identityKey, secret: identitySecret.value); diff --git a/lib/contact_invitation/contact_invitation.dart b/lib/contact_invitation/contact_invitation.dart index 83d1303..d46a57e 100644 --- a/lib/contact_invitation/contact_invitation.dart +++ b/lib/contact_invitation/contact_invitation.dart @@ -1 +1,2 @@ export 'views/views.dart'; +export 'repository/contact_invitation_repository.dart'; diff --git a/lib/contact_invitation/models/accepted_contact.dart b/lib/contact_invitation/models/accepted_contact.dart new file mode 100644 index 0000000..3f60811 --- /dev/null +++ b/lib/contact_invitation/models/accepted_contact.dart @@ -0,0 +1,19 @@ +import 'package:meta/meta.dart'; +import 'package:veilid_support/veilid_support.dart'; + +import '../../proto/proto.dart' as proto; + +@immutable +class AcceptedContact { + const AcceptedContact({ + required this.profile, + required this.remoteIdentity, + required this.remoteConversationRecordKey, + required this.localConversationRecordKey, + }); + + final proto.Profile profile; + final IdentityMaster remoteIdentity; + final TypedKey remoteConversationRecordKey; + final TypedKey localConversationRecordKey; +} diff --git a/lib/contact_invitation/models/models.dart b/lib/contact_invitation/models/models.dart new file mode 100644 index 0000000..0936f63 --- /dev/null +++ b/lib/contact_invitation/models/models.dart @@ -0,0 +1,2 @@ +export 'accepted_contact.dart'; +export 'valid_contact_invitation.dart'; diff --git a/lib/old_to_refactor/providers/contact_invitation_list_manager.dart b/lib/contact_invitation/repository/contact_invitation_repository.dart similarity index 73% rename from lib/old_to_refactor/providers/contact_invitation_list_manager.dart rename to lib/contact_invitation/repository/contact_invitation_repository.dart index 04cb273..2e8d49c 100644 --- a/lib/old_to_refactor/providers/contact_invitation_list_manager.dart +++ b/lib/contact_invitation/repository/contact_invitation_repository.dart @@ -1,16 +1,11 @@ -import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:fixnum/fixnum.dart'; import 'package:flutter/foundation.dart'; -import 'package:riverpod_annotation/riverpod_annotation.dart'; -import 'package:mutex/mutex.dart'; +import 'package:veilid_support/veilid_support.dart'; -import '../../entities/entities.dart'; +import '../../account_manager/account_manager.dart'; import '../../proto/proto.dart' as proto; import '../../tools/tools.dart'; -import '../../../packages/veilid_support/veilid_support.dart'; -import 'account.dart'; - -part 'contact_invitation_list_manager.g.dart'; +import '../models/models.dart'; ////////////////////////////////////////////////// @@ -24,22 +19,6 @@ typedef GetEncryptionKeyCallback = Future Function( EncryptionKeyType encryptionKeyType, Uint8List encryptedSecret); -////////////////////////////////////////////////// -@immutable -class AcceptedContact { - const AcceptedContact({ - required this.profile, - required this.remoteIdentity, - required this.remoteConversationRecordKey, - required this.localConversationRecordKey, - }); - - final proto.Profile profile; - final IdentityMaster remoteIdentity; - final TypedKey remoteConversationRecordKey; - final TypedKey localConversationRecordKey; -} - @immutable class InvitationStatus { const InvitationStatus({required this.acceptedContact}); @@ -50,75 +29,65 @@ class InvitationStatus { ////////////////////////////////////////////////// // Mutable state for per-account contact invitations -@riverpod -class ContactInvitationListManager extends _$ContactInvitationListManager { - ContactInvitationListManager._({ +class ContactInvitationRepository { + ContactInvitationRepository._({ required ActiveAccountInfo activeAccountInfo, + required proto.Account account, required DHTShortArray dhtRecord, }) : _activeAccountInfo = activeAccountInfo, - _dhtRecord = dhtRecord, - _records = IList(); + _account = account, + _dhtRecord = dhtRecord; - @override - FutureOr> build( - ActiveAccountInfo activeAccountInfo) async { - // Load initial todo list from the remote repository - ref.onDispose xxxx call close and pass dhtrecord through... could use a context object - and a DHTValueChangeProvider that we watch in build that updates when dht records change - return _open(activeAccountInfo); - } - - static Future _open( - ActiveAccountInfo activeAccountInfo) async { + static Future open( + ActiveAccountInfo activeAccountInfo, proto.Account account) async { final accountRecordKey = activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey; - final dhtRecord = await DHTShortArray.openOwned( + final contactInvitationListRecordKey = proto.OwnedDHTRecordPointerProto.fromProto( - activeAccountInfo.account.contactInvitationRecords), + account.contactInvitationRecords); + + final dhtRecord = await DHTShortArray.openOwned( + contactInvitationListRecordKey, parent: accountRecordKey); - return ContactInvitationListManager._( - activeAccountInfo: activeAccountInfo, dhtRecord: dhtRecord); + return ContactInvitationRepository._( + activeAccountInfo: activeAccountInfo, + account: account, + dhtRecord: dhtRecord); } Future close() async { - state = ""; await _dhtRecord.close(); } - Future refresh() async { - for (var i = 0; i < _dhtRecord.length; i++) { - final cir = await _dhtRecord.getItem(i); - if (cir == null) { - throw Exception('Failed to get contact invitation record'); - } - _records = _records.add(proto.ContactInvitationRecord.fromBuffer(cir)); - } - } + // Future refresh() async { + // for (var i = 0; i < _dhtRecord.length; i++) { + // final cir = await _dhtRecord.getItem(i); + // if (cir == null) { + // throw Exception('Failed to get contact invitation record'); + // } + // _records = _records.add(proto.ContactInvitationRecord.fromBuffer(cir)); + // } + // } Future createInvitation( {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; + final pool = DHTRecordPool.instance; // Generate writer keypair to share with new contact final cs = await pool.veilid.bestCryptoSystem(); final contactRequestWriter = await cs.generateKeyPair(); - final conversationWriter = _activeAccountInfo.getConversationWriter(); + final conversationWriter = _activeAccountInfo.conversationWriter; // Encrypt the writer secret with the encryption key final encryptedSecret = await encryptionKeyType.encryptSecretToBytes( - secret: contactRequestWriter.secret, - cryptoKind: cs.kind(), - encryptionKey: encryptionKey, + secret: contactRequestWriter.secret, + cryptoKind: cs.kind(), + encryptionKey: encryptionKey, ); // Create local chat DHT record with the account record key as its parent @@ -127,7 +96,7 @@ class ContactInvitationListManager extends _$ContactInvitationListManager { // identity key late final Uint8List signedContactInvitationBytes; await (await pool.create( - parent: accountRecordKey, + parent: _activeAccountInfo.accountRecordKey, schema: DHTSchema.smpl(oCnt: 0, members: [ DHTSchemaMember(mKey: conversationWriter.key, mCnt: 1) ]))) @@ -136,7 +105,7 @@ class ContactInvitationListManager extends _$ContactInvitationListManager { // Make ContactRequestPrivate and encrypt with the writer secret final crpriv = proto.ContactRequestPrivate() ..writerKey = contactRequestWriter.key.toProto() - ..profile = _activeAccountInfo.account.profile + ..profile = _account.profile ..identityMasterRecordKey = _activeAccountInfo.userLogin.accountMasterRecordKey.toProto() ..chatRecordKey = localConversation.key.toProto() @@ -152,7 +121,7 @@ class ContactInvitationListManager extends _$ContactInvitationListManager { // Create DHT unicast inbox for ContactRequest await (await pool.create( - parent: accountRecordKey, + parent: _activeAccountInfo.accountRecordKey, schema: DHTSchema.smpl(oCnt: 1, members: [ DHTSchemaMember(mCnt: 1, mKey: contactRequestWriter.key) ]), @@ -168,8 +137,9 @@ class ContactInvitationListManager extends _$ContactInvitationListManager { final cinvbytes = cinv.writeToBuffer(); final scinv = proto.SignedContactInvitation() ..contactInvitation = cinvbytes - ..identitySignature = - (await cs.sign(identityKey, identitySecret, cinvbytes)).toProto(); + ..identitySignature = (await cs.sign( + conversationWriter.key, conversationWriter.secret, cinvbytes)) + .toProto(); signedContactInvitationBytes = scinv.writeToBuffer(); // Create ContactInvitationRecord @@ -185,15 +155,9 @@ class ContactInvitationListManager extends _$ContactInvitationListManager { // 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 Exception('Failed to add contact invitation record'); - } - }); + if (await _dhtRecord.tryAddItem(cinvrec.writeToBuffer()) == false) { + throw Exception('Failed to add contact invitation record'); + } }); }); @@ -203,77 +167,71 @@ class ContactInvitationListManager extends _$ContactInvitationListManager { Future deleteInvitation( {required bool accepted, required proto.ContactInvitationRecord contactInvitationRecord}) async { - final pool = await DHTRecordPool.instance(); + final pool = DHTRecordPool.instance; final accountRecordKey = _activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey; // Remove ContactInvitationRecord from account's list - await (await DHTShortArray.openOwned( + for (var i = 0; i < _dhtRecord.length; i++) { + final item = await _dhtRecord.getItemProtobuf( + proto.ContactInvitationRecord.fromBuffer, i); + if (item == null) { + throw Exception('Failed to get contact invitation record'); + } + if (item.contactRequestInbox.recordKey == + contactInvitationRecord.contactRequestInbox.recordKey) { + await _dhtRecord.tryRemoveItem(i); + break; + } + } + await (await pool.openOwned( proto.OwnedDHTRecordPointerProto.fromProto( - _activeAccountInfo.account.contactInvitationRecords), + contactInvitationRecord.contactRequestInbox), 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 Exception('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.openRead( - proto.TypedKeyProto.fromProto( - contactInvitationRecord.localConversationRecordKey), - parent: accountRecordKey)) - .delete(); - } + .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.openRead( + proto.TypedKeyProto.fromProto( + contactInvitationRecord.localConversationRecordKey), + parent: accountRecordKey)) + .delete(); + } } Future validateInvitation( {required Uint8List inviteData, required GetEncryptionKeyCallback getEncryptionKeyCallback}) async { - final accountRecordKey = - _activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey; + final pool = DHTRecordPool.instance; + // Get contact request inbox from invitation 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); ValidContactInvitation? out; - final pool = await DHTRecordPool.instance(); final cs = await pool.veilid.getCryptoSystem(contactRequestInboxKey.kind); // See if we're chatting to ourselves, if so, don't delete it here + final isSelf = _contactIdentityMaster.identityPublicKey == + _activeAccountInfo.localAccount.identityMaster.identityPublicKey; +xxx this doesn't work and the upper one doesnt either final isSelf = _records.indexWhere((cir) => proto.TypedKeyProto.fromProto(cir.contactRequestInbox.recordKey) == contactRequestInboxKey) != -1; await (await pool.openRead(contactRequestInboxKey, - parent: accountRecordKey)) + parent: _activeAccountInfo.accountRecordKey)) .maybeDeleteScope(!isSelf, (contactRequestInbox) async { // final contactRequest = await contactRequestInbox @@ -315,8 +273,8 @@ class ContactInvitationListManager extends _$ContactInvitationListManager { key: proto.CryptoKeyProto.fromProto(contactRequestPrivate.writerKey), secret: writerSecret); - out = ValidContactInvitation._( - contactInvitationManager: this, + out = ValidContactInvitation( + activeAccountInfo: _activeAccountInfo, signedContactInvitation: signedContactInvitation, contactInvitation: contactInvitation, contactRequestInboxKey: contactRequestInboxKey, @@ -334,7 +292,7 @@ class ContactInvitationListManager extends _$ContactInvitationListManager { required proto.ContactInvitationRecord contactInvitationRecord}) async { // Open the contact request inbox try { - final pool = await DHTRecordPool.instance(); + final pool = DHTRecordPool.instance; final accountRecordKey = activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey; final writerKey = @@ -436,6 +394,7 @@ class ContactInvitationListManager extends _$ContactInvitationListManager { // final ActiveAccountInfo _activeAccountInfo; + final proto.Account _account; final DHTShortArray _dhtRecord; - IList _records; + //IList _records; } diff --git a/lib/contact_invitation/valid_contact_invitation.dart b/lib/contact_invitation/repository/valid_contact_invitation.dart similarity index 77% rename from lib/contact_invitation/valid_contact_invitation.dart rename to lib/contact_invitation/repository/valid_contact_invitation.dart index ec21444..2af9c30 100644 --- a/lib/contact_invitation/valid_contact_invitation.dart +++ b/lib/contact_invitation/repository/valid_contact_invitation.dart @@ -1,9 +1,19 @@ +import 'package:meta/meta.dart'; +import 'package:veilid_support/veilid_support.dart'; + +import '../../account_manager/account_manager.dart'; +import '../../proto/proto.dart' as proto; +import '../../tools/tools.dart'; +import '../models/models.dart'; +import 'contact_invitation_repository.dart'; + ////////////////////////////////////////////////// /// class ValidContactInvitation { - ValidContactInvitation._( - {required ContactInvitationListManager contactInvitationManager, + @internal + ValidContactInvitation( + {required ActiveAccountInfo activeAccountInfo, required proto.SignedContactInvitation signedContactInvitation, required proto.ContactInvitation contactInvitation, required TypedKey contactRequestInboxKey, @@ -11,7 +21,7 @@ class ValidContactInvitation { required proto.ContactRequestPrivate contactRequestPrivate, required IdentityMaster contactIdentityMaster, required KeyPair writer}) - : _contactInvitationManager = contactInvitationManager, + : _activeAccountInfo = activeAccountInfo, _signedContactInvitation = signedContactInvitation, _contactInvitation = contactInvitation, _contactRequestInboxKey = contactRequestInboxKey, @@ -21,14 +31,12 @@ class ValidContactInvitation { _writer = writer; Future accept() async { - final pool = await DHTRecordPool.instance(); - final activeAccountInfo = _contactInvitationManager._activeAccountInfo; + final pool = DHTRecordPool.instance; try { // Ensure we don't delete this if we're trying to chat to self final isSelf = _contactIdentityMaster.identityPublicKey == - activeAccountInfo.localAccount.identityMaster.identityPublicKey; - final accountRecordKey = - activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey; + _activeAccountInfo.localAccount.identityMaster.identityPublicKey; + final accountRecordKey = _activeAccountInfo.accountRecordKey; return (await pool.openWrite(_contactRequestInboxKey, _writer, parent: accountRecordKey)) @@ -37,14 +45,14 @@ class ValidContactInvitation { // Create local conversation key for this // contact and send via contact response return createConversation( - activeAccountInfo: activeAccountInfo, + activeAccountInfo: _activeAccountInfo, remoteIdentityPublicKey: _contactIdentityMaster.identityPublicTypedKey(), callback: (localConversation) async { final contactResponse = proto.ContactResponse() ..accept = true ..remoteConversationRecordKey = localConversation.key.toProto() - ..identityMasterRecordKey = activeAccountInfo + ..identityMasterRecordKey = _activeAccountInfo .localAccount.identityMaster.masterRecordKey .toProto(); final contactResponseBytes = contactResponse.writeToBuffer(); @@ -53,9 +61,8 @@ class ValidContactInvitation { .getCryptoSystem(_contactRequestInboxKey.kind); final identitySignature = await cs.sign( - activeAccountInfo - .localAccount.identityMaster.identityPublicKey, - activeAccountInfo.userLogin.identitySecret.value, + _activeAccountInfo.conversationWriter.key, + _activeAccountInfo.conversationWriter.secret, contactResponseBytes); final signedContactResponse = proto.SignedContactResponse() @@ -86,14 +93,13 @@ class ValidContactInvitation { } Future reject() async { - final pool = await DHTRecordPool.instance(); - final activeAccountInfo = _contactInvitationManager._activeAccountInfo; + final pool = DHTRecordPool.instance; // Ensure we don't delete this if we're trying to chat to self final isSelf = _contactIdentityMaster.identityPublicKey == - activeAccountInfo.localAccount.identityMaster.identityPublicKey; + _activeAccountInfo.localAccount.identityMaster.identityPublicKey; final accountRecordKey = - activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey; + _activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey; return (await pool.openWrite(_contactRequestInboxKey, _writer, parent: accountRecordKey)) @@ -103,14 +109,14 @@ class ValidContactInvitation { final contactResponse = proto.ContactResponse() ..accept = false - ..identityMasterRecordKey = activeAccountInfo + ..identityMasterRecordKey = _activeAccountInfo .localAccount.identityMaster.masterRecordKey .toProto(); final contactResponseBytes = contactResponse.writeToBuffer(); final identitySignature = await cs.sign( - activeAccountInfo.localAccount.identityMaster.identityPublicKey, - activeAccountInfo.userLogin.identitySecret.value, + _activeAccountInfo.conversationWriter.key, + _activeAccountInfo.conversationWriter.secret, contactResponseBytes); final signedContactResponse = proto.SignedContactResponse() @@ -130,12 +136,12 @@ class ValidContactInvitation { } // - ContactInvitationListManager _contactInvitationManager; + final ActiveAccountInfo _activeAccountInfo; + final TypedKey _contactRequestInboxKey; + final IdentityMaster _contactIdentityMaster; + final KeyPair _writer; proto.SignedContactInvitation _signedContactInvitation; proto.ContactInvitation _contactInvitation; - TypedKey _contactRequestInboxKey; proto.ContactRequest _contactRequest; proto.ContactRequestPrivate _contactRequestPrivate; - IdentityMaster _contactIdentityMaster; - KeyPair _writer; } diff --git a/lib/layout/home.dart b/lib/layout/home.dart index 438ec6a..8826958 100644 --- a/lib/layout/home.dart +++ b/lib/layout/home.dart @@ -7,7 +7,6 @@ import 'package:flutter_translate/flutter_translate.dart'; import 'package:go_router/go_router.dart'; import 'package:veilid_support/veilid_support.dart'; -import '../../proto/proto.dart' as proto; import '../account_manager/account_manager.dart'; import '../chat/chat.dart'; import '../theme/theme.dart'; @@ -115,7 +114,7 @@ class HomePageState extends State with TickerProviderStateMixin { } final account = AccountRepository.instance - .getAccountInfo(accountMasterRecordKey: activeUserLogin); + .getAccountInfo(accountMasterRecordKey: activeUserLogin)!; switch (account.status) { case AccountInfoStatus.noAccount: @@ -152,7 +151,7 @@ class HomePageState extends State with TickerProviderStateMixin { context, localAccounts, activeUserLogin, - account.accountRecord!, + account.activeAccountInfo!.accountRecord, ); } }); diff --git a/lib/layout/main_pager/account.dart b/lib/layout/main_pager/account_page.dart similarity index 98% rename from lib/layout/main_pager/account.dart rename to lib/layout/main_pager/account_page.dart index 3f25171..ef83d8e 100644 --- a/lib/layout/main_pager/account.dart +++ b/lib/layout/main_pager/account_page.dart @@ -1,5 +1,3 @@ -// ignore_for_file: prefer_const_constructors - import 'package:awesome_extensions/awesome_extensions.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:flutter/foundation.dart'; @@ -58,6 +56,8 @@ class AccountPageState extends State { final textTheme = theme.textTheme; final scale = theme.extension()!; + final records = widget.account.contactInvitationRecords; + final contactInvitationRecordList = ref.watch(fetchContactInvitationRecordsProvider).asData?.value ?? const IListConst([]); diff --git a/lib/layout/main_pager/chats.dart b/lib/layout/main_pager/chats_page.dart similarity index 100% rename from lib/layout/main_pager/chats.dart rename to lib/layout/main_pager/chats_page.dart diff --git a/packages/veilid_support/lib/dht_support/src/dht_short_array.dart b/packages/veilid_support/lib/dht_support/src/dht_short_array.dart index 136520c..55f28e3 100644 --- a/packages/veilid_support/lib/dht_support/src/dht_short_array.dart +++ b/packages/veilid_support/lib/dht_support/src/dht_short_array.dart @@ -497,6 +497,9 @@ class DHTShortArray { } Future trySwapItem(int aPos, int bPos) async { + if (aPos == bPos) { + return false; + } await _refreshHead(onlyUpdates: true); final oldHead = _DHTShortArrayCache.from(_head); @@ -517,6 +520,9 @@ class DHTShortArray { _head = oldHead; return false; } + + // A change happened, notify any listeners + _watchController?.sink.add(null); return true; } @@ -539,7 +545,12 @@ class DHTShortArray { return null; } - return record!.get(subkey: recordSubkey); + final result = await record!.get(subkey: recordSubkey); + if (result != null) { + // A change happened, notify any listeners + _watchController?.sink.add(null); + } + return result; } on Exception catch (_) { // Exception on write means state needs to be reverted _head = oldHead; @@ -575,6 +586,10 @@ class DHTShortArray { _head = oldHead; return false; } + + // A change happened, notify any listeners + _watchController?.sink.add(null); + return true; } @@ -591,7 +606,13 @@ class DHTShortArray { final record = await _getOrCreateLinkedRecord(recordNumber); final recordSubkey = (index % _stride) + ((recordNumber == 0) ? 1 : 0); - return record.tryWriteBytes(newValue, subkey: recordSubkey); + final result = await record.tryWriteBytes(newValue, subkey: recordSubkey); + + if (result != null) { + // A change happened, notify any listeners + _watchController?.sink.add(null); + } + return result; } Future eventualWriteItem(int pos, Uint8List newValue) async { From 7cf44ef192e86a335f8dc1b6ab2704254cc52470 Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Fri, 26 Jan 2024 21:02:11 -0500 Subject: [PATCH 16/68] refactor --- .../contact_invitation.dart | 2 +- lib/contact_invitation/models/models.dart | 2 +- .../contact_invitation_repository.dart | 8 ++ .../views/send_invite_dialog.dart | 2 +- lib/layout/edit_account.dart | 1 - lib/layout/edit_contact.dart | 30 ------- lib/layout/home/home.dart | 87 +++++++++++++++++++ lib/layout/home/home_account_invalid.dart | 32 +++++++ lib/layout/home/home_account_locked.dart | 23 +++++ lib/layout/home/home_account_missing.dart | 33 +++++++ .../home_account_ready}/chat_only.dart | 4 +- .../home_account_ready.dart} | 83 +++++++++++------- .../main_pager/account_page.dart | 10 +-- .../bottom_sheet_action_button.dart | 0 .../main_pager/chats_page.dart | 6 +- .../main_pager/main_pager.dart | 14 +-- lib/layout/home/home_no_active.dart | 25 ++++++ lib/layout/layout.dart | 2 +- .../lib/dht_support/src/dht_record_pool.dart | 10 ++- .../lib/src/async_tag_lock.dart | 45 ++++++++++ .../veilid_support/lib/veilid_support.dart | 1 + 21 files changed, 338 insertions(+), 82 deletions(-) delete mode 100644 lib/layout/edit_account.dart delete mode 100644 lib/layout/edit_contact.dart create mode 100644 lib/layout/home/home.dart create mode 100644 lib/layout/home/home_account_invalid.dart create mode 100644 lib/layout/home/home_account_locked.dart create mode 100644 lib/layout/home/home_account_missing.dart rename lib/layout/{ => home/home_account_ready}/chat_only.dart (92%) rename lib/layout/{home.dart => home/home_account_ready/home_account_ready.dart} (77%) rename lib/layout/{ => home/home_account_ready}/main_pager/account_page.dart (92%) rename lib/layout/{ => home/home_account_ready}/main_pager/bottom_sheet_action_button.dart (100%) rename lib/layout/{ => home/home_account_ready}/main_pager/chats_page.dart (94%) rename lib/layout/{ => home/home_account_ready}/main_pager/main_pager.dart (96%) create mode 100644 lib/layout/home/home_no_active.dart create mode 100644 packages/veilid_support/lib/src/async_tag_lock.dart diff --git a/lib/contact_invitation/contact_invitation.dart b/lib/contact_invitation/contact_invitation.dart index d46a57e..9ca8bca 100644 --- a/lib/contact_invitation/contact_invitation.dart +++ b/lib/contact_invitation/contact_invitation.dart @@ -1,2 +1,2 @@ -export 'views/views.dart'; export 'repository/contact_invitation_repository.dart'; +export 'views/views.dart'; diff --git a/lib/contact_invitation/models/models.dart b/lib/contact_invitation/models/models.dart index 0936f63..331af2d 100644 --- a/lib/contact_invitation/models/models.dart +++ b/lib/contact_invitation/models/models.dart @@ -1,2 +1,2 @@ export 'accepted_contact.dart'; -export 'valid_contact_invitation.dart'; +export '../repository/valid_contact_invitation.dart'; diff --git a/lib/contact_invitation/repository/contact_invitation_repository.dart b/lib/contact_invitation/repository/contact_invitation_repository.dart index 2e8d49c..fe505df 100644 --- a/lib/contact_invitation/repository/contact_invitation_repository.dart +++ b/lib/contact_invitation/repository/contact_invitation_repository.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:fixnum/fixnum.dart'; import 'package:flutter/foundation.dart'; import 'package:veilid_support/veilid_support.dart'; @@ -27,6 +29,7 @@ class InvitationStatus { ////////////////////////////////////////////////// + ////////////////////////////////////////////////// // Mutable state for per-account contact invitations class ContactInvitationRepository { @@ -37,9 +40,14 @@ class ContactInvitationRepository { }) : _activeAccountInfo = activeAccountInfo, _account = account, _dhtRecord = dhtRecord; + + void dispose() { + unawaited(close()); + } static Future open( ActiveAccountInfo activeAccountInfo, proto.Account account) async { + final accountRecordKey = activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey; diff --git a/lib/contact_invitation/views/send_invite_dialog.dart b/lib/contact_invitation/views/send_invite_dialog.dart index 417a18a..3d15d5f 100644 --- a/lib/contact_invitation/views/send_invite_dialog.dart +++ b/lib/contact_invitation/views/send_invite_dialog.dart @@ -136,7 +136,7 @@ class SendInviteDialogState extends State { navigator.pop(); return; } - final generator = createContactInvitation( + final generator = ContactInvitationRespositoryxxx.createContactInvitation( activeAccountInfo: activeAccountInfo, encryptionKeyType: _encryptionKeyType, encryptionKey: _encryptionKey, diff --git a/lib/layout/edit_account.dart b/lib/layout/edit_account.dart deleted file mode 100644 index 8b13789..0000000 --- a/lib/layout/edit_account.dart +++ /dev/null @@ -1 +0,0 @@ - diff --git a/lib/layout/edit_contact.dart b/lib/layout/edit_contact.dart deleted file mode 100644 index 480ff1f..0000000 --- a/lib/layout/edit_contact.dart +++ /dev/null @@ -1,30 +0,0 @@ -import 'package:flutter/material.dart'; - -class ContactsPage extends StatelessWidget { - const ContactsPage({super.key}); - static const path = '/contacts'; - - @override - Widget build( - BuildContext context, - ) => - const Scaffold( - body: Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text('Contacts Page'), - // ElevatedButton( - // onPressed: () async { - // ref.watch(authNotifierProvider.notifier).login( - // "myEmail", - // "myPassword", - // ); - // }, - // child: const Text("Login"), - // ), - ], - ), - ), - ); -} diff --git a/lib/layout/home/home.dart b/lib/layout/home/home.dart new file mode 100644 index 0000000..fb98287 --- /dev/null +++ b/lib/layout/home/home.dart @@ -0,0 +1,87 @@ +import 'package:fast_immutable_collections/fast_immutable_collections.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:veilid_support/veilid_support.dart'; + +import '../../account_manager/account_manager.dart'; +import '../../theme/theme.dart'; +import '../../tools/tools.dart'; +import 'home_account_invalid.dart'; +import 'home_account_locked.dart'; +import 'home_account_missing.dart'; +import 'home_account_ready.dart'; +import 'home_account_ready/home_account_ready.dart'; + +class HomePage extends StatefulWidget { + const HomePage({super.key}); + + @override + HomePageState createState() => HomePageState(); +} + +class HomePageState extends State with TickerProviderStateMixin { + final _unfocusNode = FocusNode(); + + @override + void initState() { + super.initState(); + + WidgetsBinding.instance.addPostFrameCallback((_) async { + await changeWindowSetup( + TitleBarStyle.normal, OrientationCapability.normal); + }); + } + + @override + void dispose() { + _unfocusNode.dispose(); + super.dispose(); + } + + Widget buildWithLogin(BuildContext context, IList localAccounts, + Typed? activeUserLogin) { + if (activeUserLogin == null) { + // If no logged in user is active, show the loading panel + return waitingPage(context); + } + + final accountInfo = AccountRepository.instance + .getAccountInfo(accountMasterRecordKey: activeUserLogin)!; + + switch (accountInfo.status) { + case AccountInfoStatus.noAccount: + return const HomeAccountMissing(); + case AccountInfoStatus.accountInvalid: + return const HomeAccountInvalid(); + case AccountInfoStatus.accountLocked: + return const HomeAccountLocked(); + case AccountInfoStatus.accountReady: + return BlocProvider( + create: (context) => AccountRecordCubit( + record: accountInfo.activeAccountInfo!.accountRecord), + child: context.watch().state.builder( + (context, account) => HomeAccountReady( + localAccounts: localAccounts, + activeUserLogin: activeUserLogin, + activeAccountInfo: accountInfo.activeAccountInfo!, + account: account))); + } + } + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final scale = theme.extension()!; + final activeUserLogin = context.watch().state; + final localAccounts = context.watch().state; + + return SafeArea( + child: GestureDetector( + onTap: () => FocusScope.of(context).requestFocus(_unfocusNode), + child: DecoratedBox( + decoration: BoxDecoration( + color: scale.primaryScale.activeElementBackground), + child: + buildWithLogin(context, localAccounts, activeUserLogin)))); + } +} diff --git a/lib/layout/home/home_account_invalid.dart b/lib/layout/home/home_account_invalid.dart new file mode 100644 index 0000000..bf11735 --- /dev/null +++ b/lib/layout/home/home_account_invalid.dart @@ -0,0 +1,32 @@ +import 'package:flutter/material.dart'; + +class HomeAccountInvalid extends StatefulWidget { + const HomeAccountInvalid({super.key}); + + @override + HomeAccountInvalidState createState() => HomeAccountInvalidState(); +} + +class HomeAccountInvalidState extends State { + @override + void initState() { + super.initState(); + } + + @override + void dispose() { + super.dispose(); + } + + @override + Widget build(BuildContext context) => const Text('Account invalid'); +} +// xxx: delete invalid account + // Future.delayed(0.ms, () async { + // await showErrorModal(context, translate('home.invalid_account_title'), + // translate('home.invalid_account_text')); + // // Delete account + // await AccountRepository.instance.deleteLocalAccount(activeUserLogin); + // // Switch to no active user login + // await AccountRepository.instance.switchToAccount(null); + // }); diff --git a/lib/layout/home/home_account_locked.dart b/lib/layout/home/home_account_locked.dart new file mode 100644 index 0000000..0b8a4f7 --- /dev/null +++ b/lib/layout/home/home_account_locked.dart @@ -0,0 +1,23 @@ +import 'package:flutter/material.dart'; + +class HomeAccountLocked extends StatefulWidget { + const HomeAccountLocked({super.key}); + + @override + HomeAccountLockedState createState() => HomeAccountLockedState(); +} + +class HomeAccountLockedState extends State { + @override + void initState() { + super.initState(); + } + + @override + void dispose() { + super.dispose(); + } + + @override + Widget build(BuildContext context) => const Text('Account locked'); +} diff --git a/lib/layout/home/home_account_missing.dart b/lib/layout/home/home_account_missing.dart new file mode 100644 index 0000000..d9c0aad --- /dev/null +++ b/lib/layout/home/home_account_missing.dart @@ -0,0 +1,33 @@ +import 'package:flutter/material.dart'; + +class HomeAccountMissing extends StatefulWidget { + const HomeAccountMissing({super.key}); + + @override + HomeAccountMissingState createState() => HomeAccountMissingState(); +} + +class HomeAccountMissingState extends State { + @override + void initState() { + super.initState(); + } + + @override + void dispose() { + super.dispose(); + } + + @override + Widget build(BuildContext context) => const Text('Account missing'); +} + +// xxx click to delete missing account or add to postframecallback + // Future.delayed(0.ms, () async { + // await showErrorModal(context, translate('home.missing_account_title'), + // translate('home.missing_account_text')); + // // Delete account + // await AccountRepository.instance.deleteLocalAccount(activeUserLogin); + // // Switch to no active user login + // await AccountRepository.instance.switchToAccount(null); + // }); \ No newline at end of file diff --git a/lib/layout/chat_only.dart b/lib/layout/home/home_account_ready/chat_only.dart similarity index 92% rename from lib/layout/chat_only.dart rename to lib/layout/home/home_account_ready/chat_only.dart index 6f4e645..48cb02e 100644 --- a/lib/layout/chat_only.dart +++ b/lib/layout/home/home_account_ready/chat_only.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; -import '../chat/chat.dart'; -import '../tools/tools.dart'; +import '../../../chat/chat.dart'; +import '../../../tools/tools.dart'; class ChatOnlyPage extends StatefulWidget { const ChatOnlyPage({super.key}); diff --git a/lib/layout/home.dart b/lib/layout/home/home_account_ready/home_account_ready.dart similarity index 77% rename from lib/layout/home.dart rename to lib/layout/home/home_account_ready/home_account_ready.dart index 8826958..418d248 100644 --- a/lib/layout/home.dart +++ b/lib/layout/home/home_account_ready/home_account_ready.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:awesome_extensions/awesome_extensions.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:flutter/material.dart'; @@ -7,36 +9,61 @@ import 'package:flutter_translate/flutter_translate.dart'; import 'package:go_router/go_router.dart'; import 'package:veilid_support/veilid_support.dart'; -import '../account_manager/account_manager.dart'; -import '../chat/chat.dart'; -import '../theme/theme.dart'; -import '../tools/tools.dart'; -import 'main_pager/main_pager.dart'; +import '../../../account_manager/account_manager.dart'; +import '../../../contact_invitation/contact_invitation.dart'; +import '../../../proto/proto.dart' as proto; +import '../../../theme/theme.dart'; +import '../../../tools/tools.dart'; -class HomePage extends StatefulWidget { - const HomePage({super.key}); +class HomeAccountReady extends StatefulWidget { + const HomeAccountReady( + {required IList localAccounts, + required TypedKey activeUserLogin, + required ActiveAccountInfo activeAccountInfo, + required proto.Account account, + super.key}) + : _localAccounts = localAccounts, + _activeUserLogin = activeUserLogin, + _activeAccountInfo = activeAccountInfo, + _account = account; + + final IList _localAccounts; + final TypedKey _activeUserLogin; + final ActiveAccountInfo _activeAccountInfo; + final proto.Account _account; @override - HomePageState createState() => HomePageState(); + HomeAccountReadyState createState() => HomeAccountReadyState(); } -class HomePageState extends State with TickerProviderStateMixin { - final _unfocusNode = FocusNode(); +class HomeAccountReadyState extends State + with TickerProviderStateMixin { + // + ContactInvitationRepository? _contactInvitationRepository; + // @override void initState() { super.initState(); - WidgetsBinding.instance.addPostFrameCallback((_) async { - await changeWindowSetup( - TitleBarStyle.normal, OrientationCapability.normal); + // Async initialize repositories for the active user + // xxx: this should not be necessary + // xxx: but RepositoryProvider doesn't call dispose() + Future.delayed(Duration.zero, () async { + // + final cir = await ContactInvitationRepository.open( + widget._activeAccountInfo, widget._account); + + setState(() { + _contactInvitationRepository = cir; + }); }); } @override void dispose() { - _unfocusNode.dispose(); super.dispose(); + _contactInvitationRepository?.dispose(); } // ignore: prefer_expression_function_bodies @@ -65,6 +92,8 @@ class HomePageState extends State with TickerProviderStateMixin { final theme = Theme.of(context); final scale = theme.extension()!; +xxx get rid of the cubit here and + return BlocProvider( create: (context) => AccountRecordCubit(record: accountRecord), child: Column(children: [ @@ -104,6 +133,8 @@ class HomePageState extends State with TickerProviderStateMixin { ])); } +xxx get rid of this whole function + Widget buildUserPanel() => Builder(builder: (context) { final activeUserLogin = context.watch().state; final localAccounts = context.watch().state; @@ -190,21 +221,15 @@ class HomePageState extends State with TickerProviderStateMixin { @override Widget build(BuildContext context) { - final theme = Theme.of(context); - final scale = theme.extension()!; + if (_contactInvitationRepository == null) { + return waitingPage(context); + } - return SafeArea( - child: GestureDetector( - onTap: () => FocusScope.of(context).requestFocus(_unfocusNode), - child: DecoratedBox( - decoration: BoxDecoration( - color: scale.primaryScale.activeElementBackground), - child: responsiveVisibility( - context: context, - phone: false, - ) - ? buildTablet() - : buildPhone(), - ))); + return responsiveVisibility( + context: context, + phone: false, + ) + ? buildTablet() + : buildPhone(); } } diff --git a/lib/layout/main_pager/account_page.dart b/lib/layout/home/home_account_ready/main_pager/account_page.dart similarity index 92% rename from lib/layout/main_pager/account_page.dart rename to lib/layout/home/home_account_ready/main_pager/account_page.dart index ef83d8e..3ebf2be 100644 --- a/lib/layout/main_pager/account_page.dart +++ b/lib/layout/home/home_account_ready/main_pager/account_page.dart @@ -5,11 +5,11 @@ import 'package:flutter/material.dart'; import 'package:flutter_translate/flutter_translate.dart'; import 'package:veilid_support/veilid_support.dart'; -import '../../../proto/proto.dart' as proto; -import '../../account_manager/account_manager.dart'; -import '../../contact_invitation/contact_invitation.dart'; -import '../../contacts/contacts.dart'; -import '../../theme/theme.dart'; +import '../../../../proto/proto.dart' as proto; +import '../../../account_manager/account_manager.dart'; +import '../../../contact_invitation/contact_invitation.dart'; +import '../../../contacts/contacts.dart'; +import '../../../theme/theme.dart'; class AccountPage extends StatefulWidget { const AccountPage({ diff --git a/lib/layout/main_pager/bottom_sheet_action_button.dart b/lib/layout/home/home_account_ready/main_pager/bottom_sheet_action_button.dart similarity index 100% rename from lib/layout/main_pager/bottom_sheet_action_button.dart rename to lib/layout/home/home_account_ready/main_pager/bottom_sheet_action_button.dart diff --git a/lib/layout/main_pager/chats_page.dart b/lib/layout/home/home_account_ready/main_pager/chats_page.dart similarity index 94% rename from lib/layout/main_pager/chats_page.dart rename to lib/layout/home/home_account_ready/main_pager/chats_page.dart index 56756e2..e227e8b 100644 --- a/lib/layout/main_pager/chats_page.dart +++ b/lib/layout/home/home_account_ready/main_pager/chats_page.dart @@ -2,9 +2,9 @@ import 'package:awesome_extensions/awesome_extensions.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:flutter/material.dart'; -import '../../../proto/proto.dart' as proto; -import '../../account_manager/account_manager.dart'; -import '../../tools/tools.dart'; +import '../../../../proto/proto.dart' as proto; +import '../../../account_manager/account_manager.dart'; +import '../../../tools/tools.dart'; class ChatsPage extends StatefulWidget { const ChatsPage({super.key}); diff --git a/lib/layout/main_pager/main_pager.dart b/lib/layout/home/home_account_ready/main_pager/main_pager.dart similarity index 96% rename from lib/layout/main_pager/main_pager.dart rename to lib/layout/home/home_account_ready/main_pager/main_pager.dart index 05e5979..c872224 100644 --- a/lib/layout/main_pager/main_pager.dart +++ b/lib/layout/home/home_account_ready/main_pager/main_pager.dart @@ -11,14 +11,14 @@ import 'package:stylish_bottom_bar/model/bar_items.dart'; import 'package:stylish_bottom_bar/stylish_bottom_bar.dart'; import 'package:veilid_support/veilid_support.dart'; -import '../../../proto/proto.dart' as proto; -import '../../../tools/tools.dart'; -import '../../account_manager/account_manager.dart'; -import '../../contact_invitation/contact_invitation.dart'; -import '../../theme/theme.dart'; -import 'account.dart'; +import '../../../../proto/proto.dart' as proto; +import '../../../../tools/tools.dart'; +import '../../../account_manager/account_manager.dart'; +import '../../../contact_invitation/contact_invitation.dart'; +import '../../../theme/theme.dart'; +import 'account_page.dart'; import 'bottom_sheet_action_button.dart'; -import 'chats.dart'; +import 'chats_page.dart'; class MainPager extends StatefulWidget { const MainPager( diff --git a/lib/layout/home/home_no_active.dart b/lib/layout/home/home_no_active.dart new file mode 100644 index 0000000..31e3378 --- /dev/null +++ b/lib/layout/home/home_no_active.dart @@ -0,0 +1,25 @@ +import 'package:flutter/material.dart'; + +import '../../tools/tools.dart'; + +class HomeNoActive extends StatefulWidget { + const HomeNoActive({super.key}); + + @override + HomeNoActiveState createState() => HomeNoActiveState(); +} + +class HomeNoActiveState extends State { + @override + void initState() { + super.initState(); + } + + @override + void dispose() { + super.dispose(); + } + + @override + Widget build(BuildContext context) => waitingPage(context); +} diff --git a/lib/layout/layout.dart b/lib/layout/layout.dart index 34b7364..e896be6 100644 --- a/lib/layout/layout.dart +++ b/lib/layout/layout.dart @@ -1,4 +1,4 @@ -export 'chat_only.dart'; +export 'home/home_account_ready/chat_only.dart'; export 'default_app_bar.dart'; export 'edit_account.dart'; export 'edit_contact.dart'; diff --git a/packages/veilid_support/lib/dht_support/src/dht_record_pool.dart b/packages/veilid_support/lib/dht_support/src/dht_record_pool.dart index 1c09d45..fd47da4 100644 --- a/packages/veilid_support/lib/dht_support/src/dht_record_pool.dart +++ b/packages/veilid_support/lib/dht_support/src/dht_record_pool.dart @@ -60,11 +60,14 @@ class DHTRecordPool with TableDBBacked { parentByChild: IMap(), rootRecords: ISet()), _opened = {}, + _locks = AsyncTagLock(), _routingContext = routingContext, _veilid = veilid; // Persistent DHT record list DHTRecordPoolAllocations _state; + // Lock table to ensure we don't open the same record more than once + final AsyncTagLock _locks; // Which DHT records are currently open final Map _opened; // Default routing context to use for new keys @@ -115,6 +118,7 @@ class DHTRecordPool with TableDBBacked { if (rec == null) { throw StateError('record already closed'); } + _locks.unlockTag(key); } Future deleteDeep(TypedKey parent) async { @@ -247,6 +251,8 @@ class DHTRecordPool with TableDBBacked { TypedKey? parent, int defaultSubkey = 0, DHTRecordCrypto? crypto}) async { + await _locks.lockTag(recordKey); + final dhtctx = routingContext ?? _routingContext; late final DHTRecord rec; @@ -278,6 +284,8 @@ class DHTRecordPool with TableDBBacked { int defaultSubkey = 0, DHTRecordCrypto? crypto, }) async { + await _locks.lockTag(recordKey); + final dhtctx = routingContext ?? _routingContext; late final DHTRecord rec; @@ -325,7 +333,7 @@ class DHTRecordPool with TableDBBacked { crypto: crypto, ); - /// Look up an opened DHRRecord + /// Look up an opened DHTRecord DHTRecord? getOpenedRecord(TypedKey recordKey) => _opened[recordKey]; /// Get the parent of a DHTRecord key if it exists diff --git a/packages/veilid_support/lib/src/async_tag_lock.dart b/packages/veilid_support/lib/src/async_tag_lock.dart new file mode 100644 index 0000000..293b045 --- /dev/null +++ b/packages/veilid_support/lib/src/async_tag_lock.dart @@ -0,0 +1,45 @@ +import 'package:mutex/mutex.dart'; + +class _AsyncTagLockEntry { + _AsyncTagLockEntry() + : mutex = Mutex(), + waitingCount = 1; + // + Mutex mutex; + int waitingCount; +} + +class AsyncTagLock { + AsyncTagLock() + : _tableLock = Mutex(), + _locks = {}; + + Future lockTag(T tag) async { + await _tableLock.protect(() async { + var lockEntry = _locks[tag]; + if (lockEntry != null) { + lockEntry.waitingCount++; + } else { + lockEntry = _locks[tag] = _AsyncTagLockEntry(); + } + + await lockEntry.mutex.acquire(); + lockEntry.waitingCount--; + }); + } + + void unlockTag(T tag) { + final lockEntry = _locks[tag]!; + if (lockEntry.waitingCount == 0) { + // If nobody is waiting for the mutex we can just drop it + _locks.remove(tag); + } else { + // Someone's waiting for the tag lock so release the mutex for it + lockEntry.mutex.release(); + } + } + + // + final Mutex _tableLock; + final Map _locks; +} diff --git a/packages/veilid_support/lib/veilid_support.dart b/packages/veilid_support/lib/veilid_support.dart index fec2539..b0dfce0 100644 --- a/packages/veilid_support/lib/veilid_support.dart +++ b/packages/veilid_support/lib/veilid_support.dart @@ -6,6 +6,7 @@ library veilid_support; export 'package:veilid/veilid.dart'; export 'dht_support/dht_support.dart'; +export 'src/async_tag_lock.dart'; export 'src/async_value.dart'; export 'src/config.dart'; export 'src/identity.dart'; From 20047a956fd482b3f2fcc540532576f4e64abfcd Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Sat, 27 Jan 2024 20:10:30 -0500 Subject: [PATCH 17/68] more refactor --- .../contact_invitation_list_cubit.dart} | 18 +- lib/contact_invitation/cubits/cubits.dart | 1 + .../valid_contact_invitation.dart | 0 .../paste_invite_dialog.dart | 131 ------ .../scan_invite_dialog.dart | 399 ------------------ lib/layout/home/home.dart | 11 +- .../home_account_ready.dart | 217 +++------- .../main_pager/account_page.dart | 21 +- lib/layout/layout.dart | 6 +- .../providers/conversation.dart | 11 +- .../lib/dht_support/dht_support.dart | 1 + .../src/dht_short_array_cubit.dart | 26 +- pubspec.lock | 2 +- pubspec.yaml | 1 + 14 files changed, 113 insertions(+), 732 deletions(-) rename lib/contact_invitation/{repository/contact_invitation_repository.dart => cubits/contact_invitation_list_cubit.dart} (97%) create mode 100644 lib/contact_invitation/cubits/cubits.dart rename lib/contact_invitation/{repository => models}/valid_contact_invitation.dart (100%) delete mode 100644 lib/contact_invitation/paste_invite_dialog.dart delete mode 100644 lib/contact_invitation/scan_invite_dialog.dart diff --git a/lib/contact_invitation/repository/contact_invitation_repository.dart b/lib/contact_invitation/cubits/contact_invitation_list_cubit.dart similarity index 97% rename from lib/contact_invitation/repository/contact_invitation_repository.dart rename to lib/contact_invitation/cubits/contact_invitation_list_cubit.dart index fe505df..1e008de 100644 --- a/lib/contact_invitation/repository/contact_invitation_repository.dart +++ b/lib/contact_invitation/cubits/contact_invitation_list_cubit.dart @@ -1,5 +1,6 @@ import 'dart:async'; +import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:fixnum/fixnum.dart'; import 'package:flutter/foundation.dart'; import 'package:veilid_support/veilid_support.dart'; @@ -29,22 +30,19 @@ class InvitationStatus { ////////////////////////////////////////////////// - ////////////////////////////////////////////////// // Mutable state for per-account contact invitations -class ContactInvitationRepository { - ContactInvitationRepository._({ + +class ContactInvitationListCubit extends DHTShortArrayCubit { + ContactInvitationListCubit({ required ActiveAccountInfo activeAccountInfo, required proto.Account account, required DHTShortArray dhtRecord, }) : _activeAccountInfo = activeAccountInfo, _account = account, - _dhtRecord = dhtRecord; - - void dispose() { - unawaited(close()); - } - + _dhtRecord = dhtRecord, + super(shortArray: dhtRecord, decodeElement: proto.ContactInvitation.fromBuffer); +xxx convert the rest of this to cubit static Future open( ActiveAccountInfo activeAccountInfo, proto.Account account) async { @@ -65,8 +63,10 @@ class ContactInvitationRepository { dhtRecord: dhtRecord); } + @override Future close() async { await _dhtRecord.close(); + await super.close(); } // Future refresh() async { diff --git a/lib/contact_invitation/cubits/cubits.dart b/lib/contact_invitation/cubits/cubits.dart new file mode 100644 index 0000000..c16213a --- /dev/null +++ b/lib/contact_invitation/cubits/cubits.dart @@ -0,0 +1 @@ +export 'contact_invitation_list_cubit.dart'; diff --git a/lib/contact_invitation/repository/valid_contact_invitation.dart b/lib/contact_invitation/models/valid_contact_invitation.dart similarity index 100% rename from lib/contact_invitation/repository/valid_contact_invitation.dart rename to lib/contact_invitation/models/valid_contact_invitation.dart diff --git a/lib/contact_invitation/paste_invite_dialog.dart b/lib/contact_invitation/paste_invite_dialog.dart deleted file mode 100644 index a352892..0000000 --- a/lib/contact_invitation/paste_invite_dialog.dart +++ /dev/null @@ -1,131 +0,0 @@ -import 'dart:async'; -import 'dart:typed_data'; - -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 '../old_to_refactor/tools/tools.dart'; -import '../old_to_refactor/veilid_support/veilid_support.dart'; -import 'views/invite_dialog.dart'; - -class PasteInviteDialog extends ConsumerStatefulWidget { - const PasteInviteDialog({super.key}); - - @override - PasteInviteDialogState createState() => PasteInviteDialogState(); - - static Future show(BuildContext context) async { - await showStyledDialog( - context: context, - title: translate('paste_invite_dialog.title'), - child: const PasteInviteDialog()); - } -} - -class PasteInviteDialogState extends ConsumerState { - final _pasteTextController = TextEditingController(); - - @override - void initState() { - super.initState(); - } - - Future _onPasteChanged( - String text, - Future Function({ - required Uint8List inviteData, - }) validateInviteData) async { - final lines = text.split('\n'); - if (lines.isEmpty) { - return; - } - - var firstline = - lines.indexWhere((element) => element.contains('BEGIN VEILIDCHAT')); - firstline += 1; - - var lastline = - lines.indexWhere((element) => element.contains('END VEILIDCHAT')); - if (lastline == -1) { - lastline = lines.length; - } - if (lastline <= firstline) { - return; - } - final inviteDataBase64 = lines - .sublist(firstline, lastline) - .join() - .replaceAll(RegExp(r'[^A-Za-z0-9\-_]'), ''); - final inviteData = base64UrlNoPadDecode(inviteDataBase64); - - await validateInviteData(inviteData: inviteData); - } - - void onValidationCancelled() { - _pasteTextController.clear(); - } - - void onValidationSuccess() { - //_pasteTextController.clear(); - } - - void onValidationFailed() { - _pasteTextController.clear(); - } - - bool inviteControlIsValid() => _pasteTextController.text.isNotEmpty; - - Widget buildInviteControl( - BuildContext context, - InviteDialogState dialogState, - Future Function({required Uint8List inviteData}) - validateInviteData) { - final theme = Theme.of(context); - final scale = theme.extension()!; - //final textTheme = theme.textTheme; - //final height = MediaQuery.of(context).size.height; - - final monoStyle = TextStyle( - fontFamily: 'Source Code Pro', - fontSize: 11, - color: scale.primaryScale.text, - ); - - return Column(mainAxisSize: MainAxisSize.min, children: [ - Text( - translate('paste_invite_dialog.paste_invite_here'), - ).paddingLTRB(0, 0, 0, 8), - Container( - constraints: const BoxConstraints(maxHeight: 200), - child: TextField( - enabled: !dialogState.isValidating, - onChanged: (text) async => - _onPasteChanged(text, validateInviteData), - style: monoStyle, - keyboardType: TextInputType.multiline, - maxLines: null, - controller: _pasteTextController, - decoration: const InputDecoration( - border: OutlineInputBorder(), - hintText: '--- BEGIN VEILIDCHAT CONTACT INVITE ----\n' - 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX\n' - '---- END VEILIDCHAT CONTACT INVITE -----\n', - //labelText: translate('paste_invite_dialog.paste') - ), - )).paddingLTRB(0, 0, 0, 8) - ]); - } - - @override - // ignore: prefer_expression_function_bodies - Widget build(BuildContext context) { - return InviteDialog( - onValidationCancelled: onValidationCancelled, - onValidationSuccess: onValidationSuccess, - onValidationFailed: onValidationFailed, - inviteControlIsValid: inviteControlIsValid, - buildInviteControl: buildInviteControl); - } -} diff --git a/lib/contact_invitation/scan_invite_dialog.dart b/lib/contact_invitation/scan_invite_dialog.dart deleted file mode 100644 index c7b4f76..0000000 --- a/lib/contact_invitation/scan_invite_dialog.dart +++ /dev/null @@ -1,399 +0,0 @@ -import 'dart:async'; -import 'dart:io'; -import 'dart:typed_data'; - -import 'package:awesome_extensions/awesome_extensions.dart'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/scheduler.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:flutter_translate/flutter_translate.dart'; -import 'package:image/image.dart' as img; -import 'package:mobile_scanner/mobile_scanner.dart'; -import 'package:pasteboard/pasteboard.dart'; -import 'package:zxing2/qrcode.dart'; - -import '../old_to_refactor/tools/tools.dart'; -import 'views/invite_dialog.dart'; - -class BarcodeOverlay extends CustomPainter { - BarcodeOverlay({ - required this.barcode, - required this.arguments, - required this.boxFit, - required this.capture, - }); - - final BarcodeCapture capture; - final Barcode barcode; - final MobileScannerArguments arguments; - final BoxFit boxFit; - - @override - void paint(Canvas canvas, Size size) { - if (barcode.corners == null) { - return; - } - final adjustedSize = applyBoxFit(boxFit, arguments.size, size); - - var verticalPadding = size.height - adjustedSize.destination.height; - var horizontalPadding = size.width - adjustedSize.destination.width; - if (verticalPadding > 0) { - verticalPadding = verticalPadding / 2; - } else { - verticalPadding = 0; - } - - if (horizontalPadding > 0) { - horizontalPadding = horizontalPadding / 2; - } else { - horizontalPadding = 0; - } - - final ratioWidth = - (Platform.isIOS ? capture.width! : arguments.size.width) / - adjustedSize.destination.width; - final ratioHeight = - (Platform.isIOS ? capture.height! : arguments.size.height) / - adjustedSize.destination.height; - - final adjustedOffset = []; - for (final offset in barcode.corners!) { - adjustedOffset.add( - Offset( - offset.dx / ratioWidth + horizontalPadding, - offset.dy / ratioHeight + verticalPadding, - ), - ); - } - final cutoutPath = Path()..addPolygon(adjustedOffset, true); - - final backgroundPaint = Paint() - ..color = Colors.red.withOpacity(0.3) - ..style = PaintingStyle.fill - ..blendMode = BlendMode.dstOut; - - canvas.drawPath(cutoutPath, backgroundPaint); - } - - @override - bool shouldRepaint(covariant CustomPainter oldDelegate) => false; -} - -class ScannerOverlay extends CustomPainter { - ScannerOverlay(this.scanWindow); - - final Rect scanWindow; - - @override - void paint(Canvas canvas, Size size) { - final backgroundPath = Path()..addRect(Rect.largest); - final cutoutPath = Path()..addRect(scanWindow); - - final backgroundPaint = Paint() - ..color = Colors.black.withOpacity(0.5) - ..style = PaintingStyle.fill - ..blendMode = BlendMode.dstOut; - - final backgroundWithCutout = Path.combine( - PathOperation.difference, - backgroundPath, - cutoutPath, - ); - canvas.drawPath(backgroundWithCutout, backgroundPaint); - } - - @override - bool shouldRepaint(covariant CustomPainter oldDelegate) => false; -} - -class ScanInviteDialog extends ConsumerStatefulWidget { - const ScanInviteDialog({super.key}); - - @override - ScanInviteDialogState createState() => ScanInviteDialogState(); - - static Future show(BuildContext context) async { - await showStyledDialog( - context: context, - title: translate('scan_invite_dialog.title'), - child: const ScanInviteDialog()); - } -} - -class ScanInviteDialogState extends ConsumerState { - bool scanned = false; - - @override - void initState() { - super.initState(); - } - - void onValidationCancelled() { - setState(() { - scanned = false; - }); - } - - void onValidationSuccess() {} - void onValidationFailed() { - setState(() { - scanned = false; - }); - } - - bool inviteControlIsValid() => false; // _pasteTextController.text.isNotEmpty; - - Future scanQRImage(BuildContext context) async { - final theme = Theme.of(context); - //final textTheme = theme.textTheme; - final scale = theme.extension()!; - final windowSize = MediaQuery.of(context).size; - //final maxDialogWidth = min(windowSize.width - 64.0, 800.0 - 64.0); - //final maxDialogHeight = windowSize.height - 64.0; - - final scanWindow = Rect.fromCenter( - center: MediaQuery.of(context).size.center(Offset.zero), - width: 200, - height: 200, - ); - - final cameraController = MobileScannerController(); - try { - return showDialog( - context: context, - builder: (context) => Stack( - fit: StackFit.expand, - children: [ - MobileScanner( - fit: BoxFit.contain, - scanWindow: scanWindow, - controller: cameraController, - errorBuilder: (context, error, child) => - ScannerErrorWidget(error: error), - onDetect: (c) { - final barcode = c.barcodes.firstOrNull; - - final barcodeBytes = barcode?.rawBytes; - if (barcodeBytes != null) { - cameraController.dispose(); - Navigator.pop(context, barcodeBytes); - } - }), - CustomPaint( - painter: ScannerOverlay(scanWindow), - ), - Align( - alignment: Alignment.bottomCenter, - child: Container( - alignment: Alignment.bottomCenter, - height: 100, - color: Colors.black.withOpacity(0.4), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - IconButton( - color: Colors.white, - icon: ValueListenableBuilder( - valueListenable: cameraController.torchState, - builder: (context, state, child) { - switch (state) { - case TorchState.off: - return Icon(Icons.flash_off, - color: - scale.grayScale.subtleBackground); - case TorchState.on: - return Icon(Icons.flash_on, - color: scale.primaryScale.background); - } - }, - ), - iconSize: 32, - onPressed: cameraController.toggleTorch, - ), - SizedBox( - width: windowSize.width - 120, - height: 50, - child: FittedBox( - child: Text( - translate('scan_invite_dialog.instructions'), - overflow: TextOverflow.fade, - style: Theme.of(context) - .textTheme - .labelLarge! - .copyWith(color: Colors.white), - ), - ), - ), - IconButton( - color: Colors.white, - icon: ValueListenableBuilder( - valueListenable: - cameraController.cameraFacingState, - builder: (context, state, child) { - switch (state) { - case CameraFacing.front: - return const Icon(Icons.camera_front); - case CameraFacing.back: - return const Icon(Icons.camera_rear); - } - }, - ), - iconSize: 32, - onPressed: cameraController.switchCamera, - ), - ], - ), - ), - ), - Align( - alignment: Alignment.topRight, - child: IconButton( - color: Colors.white, - icon: Icon(Icons.close, - color: scale.grayScale.background), - iconSize: 32, - onPressed: () => { - SchedulerBinding.instance - .addPostFrameCallback((_) { - cameraController.dispose(); - Navigator.pop(context, null); - }) - })), - ], - )); - } on MobileScannerException catch (e) { - if (e.errorCode == MobileScannerErrorCode.permissionDenied) { - showErrorToast( - context, translate('scan_invite_dialog.permission_error')); - } else { - showErrorToast(context, translate('scan_invite_dialog.error')); - } - } on Exception catch (_) { - showErrorToast(context, translate('scan_invite_dialog.error')); - } - - return null; - } - - Future pasteQRImage(BuildContext context) async { - final imageBytes = await Pasteboard.image; - if (imageBytes == null) { - if (context.mounted) { - showErrorToast(context, translate('scan_invite_dialog.not_an_image')); - } - return null; - } - - final image = img.decodeImage(imageBytes); - if (image == null) { - if (context.mounted) { - showErrorToast( - context, translate('scan_invite_dialog.could_not_decode_image')); - } - return null; - } - - try { - final source = RGBLuminanceSource( - image.width, - image.height, - image - .convert(numChannels: 4) - .getBytes(order: img.ChannelOrder.abgr) - .buffer - .asInt32List()); - final bitmap = BinaryBitmap(HybridBinarizer(source)); - - final reader = QRCodeReader(); - final result = reader.decode(bitmap); - - final segs = result.resultMetadata[ResultMetadataType.byteSegments]! - as List; - return Uint8List.fromList(segs[0].toList()); - } on Exception catch (_) { - if (context.mounted) { - showErrorToast( - context, translate('scan_invite_dialog.not_a_valid_qr_code')); - } - return null; - } - } - - Widget buildInviteControl( - BuildContext context, - InviteDialogState dialogState, - Future Function({required Uint8List inviteData}) - validateInviteData) { - //final theme = Theme.of(context); - //final scale = theme.extension()!; - //final textTheme = theme.textTheme; - //final height = MediaQuery.of(context).size.height; - - if (isiOS || isAndroid) { - return Column(mainAxisSize: MainAxisSize.min, children: [ - if (!scanned) - Text( - translate('scan_invite_dialog.scan_qr_here'), - ).paddingLTRB(0, 0, 0, 8), - if (!scanned) - Container( - constraints: const BoxConstraints(maxHeight: 200), - child: ElevatedButton( - onPressed: dialogState.isValidating - ? null - : () async { - final inviteData = await scanQRImage(context); - if (inviteData != null) { - setState(() { - scanned = true; - }); - await validateInviteData(inviteData: inviteData); - } - }, - child: Text(translate('scan_invite_dialog.scan'))), - ).paddingLTRB(0, 0, 0, 8) - ]); - } - return Column(mainAxisSize: MainAxisSize.min, children: [ - if (!scanned) - Text( - translate('scan_invite_dialog.paste_qr_here'), - ).paddingLTRB(0, 0, 0, 8), - if (!scanned) - Container( - constraints: const BoxConstraints(maxHeight: 200), - child: ElevatedButton( - onPressed: dialogState.isValidating - ? null - : () async { - final inviteData = await pasteQRImage(context); - if (inviteData != null) { - await validateInviteData(inviteData: inviteData); - setState(() { - scanned = true; - }); - } - }, - child: Text(translate('scan_invite_dialog.paste'))), - ).paddingLTRB(0, 0, 0, 8) - ]); - } - - @override - // ignore: prefer_expression_function_bodies - Widget build(BuildContext context) { - return InviteDialog( - onValidationCancelled: onValidationCancelled, - onValidationSuccess: onValidationSuccess, - onValidationFailed: onValidationFailed, - inviteControlIsValid: inviteControlIsValid, - buildInviteControl: buildInviteControl); - } - - @override - void debugFillProperties(DiagnosticPropertiesBuilder properties) { - super.debugFillProperties(properties); - properties.add(DiagnosticsProperty('scanned', scanned)); - } -} diff --git a/lib/layout/home/home.dart b/lib/layout/home/home.dart index fb98287..60ba0d3 100644 --- a/lib/layout/home/home.dart +++ b/lib/layout/home/home.dart @@ -9,8 +9,8 @@ import '../../tools/tools.dart'; import 'home_account_invalid.dart'; import 'home_account_locked.dart'; import 'home_account_missing.dart'; -import 'home_account_ready.dart'; import 'home_account_ready/home_account_ready.dart'; +import 'home_no_active.dart'; class HomePage extends StatefulWidget { const HomePage({super.key}); @@ -40,9 +40,11 @@ class HomePageState extends State with TickerProviderStateMixin { Widget buildWithLogin(BuildContext context, IList localAccounts, Typed? activeUserLogin) { + final activeUserLogin = context.watch().state; + if (activeUserLogin == null) { // If no logged in user is active, show the loading panel - return waitingPage(context); + return const HomeNoActive(); } final accountInfo = AccountRepository.instance @@ -72,8 +74,6 @@ class HomePageState extends State with TickerProviderStateMixin { Widget build(BuildContext context) { final theme = Theme.of(context); final scale = theme.extension()!; - final activeUserLogin = context.watch().state; - final localAccounts = context.watch().state; return SafeArea( child: GestureDetector( @@ -81,7 +81,6 @@ class HomePageState extends State with TickerProviderStateMixin { child: DecoratedBox( decoration: BoxDecoration( color: scale.primaryScale.activeElementBackground), - child: - buildWithLogin(context, localAccounts, activeUserLogin)))); + child: buildWithLogin(context)))); } } diff --git a/lib/layout/home/home_account_ready/home_account_ready.dart b/lib/layout/home/home_account_ready/home_account_ready.dart index 418d248..af4f13e 100644 --- a/lib/layout/home/home_account_ready/home_account_ready.dart +++ b/lib/layout/home/home_account_ready/home_account_ready.dart @@ -1,9 +1,9 @@ import 'dart:async'; import 'package:awesome_extensions/awesome_extensions.dart'; +import 'package:equatable/equatable.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_animate/flutter_animate.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_translate/flutter_translate.dart'; import 'package:go_router/go_router.dart'; @@ -14,23 +14,16 @@ import '../../../contact_invitation/contact_invitation.dart'; import '../../../proto/proto.dart' as proto; import '../../../theme/theme.dart'; import '../../../tools/tools.dart'; +import 'main_pager/main_pager.dart'; class HomeAccountReady extends StatefulWidget { const HomeAccountReady( - {required IList localAccounts, - required TypedKey activeUserLogin, - required ActiveAccountInfo activeAccountInfo, - required proto.Account account, + {required ActiveAccountInfo activeAccountInfo, + required Account account, super.key}) - : _localAccounts = localAccounts, - _activeUserLogin = activeUserLogin, - _activeAccountInfo = activeAccountInfo, - _account = account; + : _accountReadyContext = accountReadyContext; - final IList _localAccounts; - final TypedKey _activeUserLogin; - final ActiveAccountInfo _activeAccountInfo; - final proto.Account _account; + final AccountReadyContext _accountReadyContext; @override HomeAccountReadyState createState() => HomeAccountReadyState(); @@ -52,7 +45,7 @@ class HomeAccountReadyState extends State Future.delayed(Duration.zero, () async { // final cir = await ContactInvitationRepository.open( - widget._activeAccountInfo, widget._account); + widget.activeAccountInfo, widget._accountReadyContext.account); setState(() { _contactInvitationRepository = cir; @@ -66,15 +59,6 @@ class HomeAccountReadyState extends State _contactInvitationRepository?.dispose(); } - // ignore: prefer_expression_function_bodies - Widget buildAccountList() { - return const Column(children: [ - Center(child: Text('Small Profile')), - Center(child: Text('Contact invitations')), - Center(child: Text('Contacts')) - ]); - } - Widget buildUnlockAccount( BuildContext context, IList localAccounts, @@ -83,141 +67,66 @@ class HomeAccountReadyState extends State return const Center(child: Text('unlock account')); } - /// We have an active, unlocked, user login - Widget buildReadyAccount( - BuildContext context, - IList localAccounts, - TypedKey activeUserLogin, - DHTRecord accountRecord) { + Widget buildUserPanel(BuildContext context) { final theme = Theme.of(context); final scale = theme.extension()!; -xxx get rid of the cubit here and - - return BlocProvider( - create: (context) => AccountRecordCubit(record: accountRecord), - child: Column(children: [ - Row(children: [ - IconButton( - icon: const Icon(Icons.settings), - color: scale.secondaryScale.text, - constraints: const BoxConstraints.expand(height: 64, width: 64), - style: ButtonStyle( - backgroundColor: - MaterialStateProperty.all(scale.secondaryScale.border), - shape: MaterialStateProperty.all( - const RoundedRectangleBorder( - borderRadius: - BorderRadius.all(Radius.circular(16))))), - tooltip: translate('app_bar.settings_tooltip'), - onPressed: () async { - context.go('/home/settings'); - }).paddingLTRB(0, 0, 8, 0), - context - .watch() - .state - .builder((context, account) => ProfileWidget( - name: account.profile.name, - pronouns: account.profile.pronouns, - )) - .expanded(), - ]).paddingAll(8), - context - .watch() - .state - .builder((context, account) => MainPager( - localAccounts: localAccounts, - activeUserLogin: activeUserLogin, - account: account)) - .expanded() - ])); + return Column(children: [ + Row(children: [ + IconButton( + icon: const Icon(Icons.settings), + color: scale.secondaryScale.text, + constraints: const BoxConstraints.expand(height: 64, width: 64), + style: ButtonStyle( + backgroundColor: + MaterialStateProperty.all(scale.secondaryScale.border), + shape: MaterialStateProperty.all(const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(16))))), + tooltip: translate('app_bar.settings_tooltip'), + onPressed: () async { + context.go('/home/settings'); + }).paddingLTRB(0, 0, 8, 0), + ProfileWidget( + name: widget._accountReadyContext.account.profile.name, + pronouns: widget._accountReadyContext.account.profile.pronouns, + ).expanded(), + ]).paddingAll(8), + MainPager().expanded() + ]); } -xxx get rid of this whole function + Widget buildPhone(BuildContext context) => + Material(color: Colors.transparent, child: buildUserPanel(context)); - Widget buildUserPanel() => Builder(builder: (context) { - final activeUserLogin = context.watch().state; - final localAccounts = context.watch().state; + Widget buildTabletLeftPane(BuildContext context) => Builder( + builder: (context) => + Material(color: Colors.transparent, child: buildUserPanel(context))); - if (activeUserLogin == null) { - // If no logged in user is active, show the loading panel - return waitingPage(context); - } - - final account = AccountRepository.instance - .getAccountInfo(accountMasterRecordKey: activeUserLogin)!; - - switch (account.status) { - case AccountInfoStatus.noAccount: - Future.delayed(0.ms, () async { - await showErrorModal( - context, - translate('home.missing_account_title'), - translate('home.missing_account_text')); - // Delete account - await AccountRepository.instance - .deleteLocalAccount(activeUserLogin); - // Switch to no active user login - await AccountRepository.instance.switchToAccount(null); - }); - return waitingPage(context); - case AccountInfoStatus.accountInvalid: - Future.delayed(0.ms, () async { - await showErrorModal( - context, - translate('home.invalid_account_title'), - translate('home.invalid_account_text')); - // Delete account - await AccountRepository.instance - .deleteLocalAccount(activeUserLogin); - // Switch to no active user login - await AccountRepository.instance.switchToAccount(null); - }); - return waitingPage(context); - case AccountInfoStatus.accountLocked: - // Show unlock widget - return buildUnlockAccount(context, localAccounts); - case AccountInfoStatus.accountReady: - return buildReadyAccount( - context, - localAccounts, - activeUserLogin, - account.activeAccountInfo!.accountRecord, - ); - } - }); - - Widget buildPhone() => - Material(color: Colors.transparent, child: buildUserPanel()); - - Widget buildTabletLeftPane() => - Material(color: Colors.transparent, child: buildUserPanel()); - - Widget buildTabletRightPane() => buildChatComponent(); + Widget buildTabletRightPane(BuildContext context) => buildChatComponent(); // ignore: prefer_expression_function_bodies - Widget buildTablet() => Builder(builder: (context) { - final w = MediaQuery.of(context).size.width; - final theme = Theme.of(context); - final scale = theme.extension()!; + Widget buildTablet(BuildContext context) { + final w = MediaQuery.of(context).size.width; + final theme = Theme.of(context); + final scale = theme.extension()!; - final children = [ - ConstrainedBox( - constraints: const BoxConstraints(minWidth: 300, maxWidth: 300), - child: ConstrainedBox( - constraints: BoxConstraints(maxWidth: w / 2), - child: buildTabletLeftPane())), - SizedBox( - width: 2, - height: double.infinity, - child: ColoredBox(color: scale.primaryScale.hoverBorder)), - Expanded(child: buildTabletRightPane()), - ]; + final children = [ + ConstrainedBox( + constraints: const BoxConstraints(minWidth: 300, maxWidth: 300), + child: ConstrainedBox( + constraints: BoxConstraints(maxWidth: w / 2), + child: buildTabletLeftPane(context))), + SizedBox( + width: 2, + height: double.infinity, + child: ColoredBox(color: scale.primaryScale.hoverBorder)), + Expanded(child: buildTabletRightPane(context)), + ]; - return Row( - children: children, - ); - }); + return Row( + children: children, + ); + } @override Widget build(BuildContext context) { @@ -225,11 +134,13 @@ xxx get rid of this whole function return waitingPage(context); } - return responsiveVisibility( - context: context, - phone: false, - ) - ? buildTablet() - : buildPhone(); + return RepositoryProvider.value( + value: _contactInvitationRepository, + child: responsiveVisibility( + context: context, + phone: false, + ) + ? buildTablet(context) + : buildPhone(context)); } } diff --git a/lib/layout/home/home_account_ready/main_pager/account_page.dart b/lib/layout/home/home_account_ready/main_pager/account_page.dart index 3ebf2be..35abe86 100644 --- a/lib/layout/home/home_account_ready/main_pager/account_page.dart +++ b/lib/layout/home/home_account_ready/main_pager/account_page.dart @@ -5,34 +5,17 @@ import 'package:flutter/material.dart'; import 'package:flutter_translate/flutter_translate.dart'; import 'package:veilid_support/veilid_support.dart'; +import '../../../../account_manager/account_manager.dart'; import '../../../../proto/proto.dart' as proto; -import '../../../account_manager/account_manager.dart'; -import '../../../contact_invitation/contact_invitation.dart'; -import '../../../contacts/contacts.dart'; -import '../../../theme/theme.dart'; +import '../../../../theme/theme.dart'; class AccountPage extends StatefulWidget { const AccountPage({ - required this.localAccounts, - required this.activeUserLogin, - required this.account, super.key, }); - final IList localAccounts; - final TypedKey activeUserLogin; - final proto.Account account; - @override AccountPageState createState() => AccountPageState(); - @override - void debugFillProperties(DiagnosticPropertiesBuilder properties) { - super.debugFillProperties(properties); - properties - ..add(IterableProperty('localAccounts', localAccounts)) - ..add(DiagnosticsProperty('activeUserLogin', activeUserLogin)) - ..add(DiagnosticsProperty('account', account)); - } } class AccountPageState extends State { diff --git a/lib/layout/layout.dart b/lib/layout/layout.dart index e896be6..90c50e7 100644 --- a/lib/layout/layout.dart +++ b/lib/layout/layout.dart @@ -1,7 +1,3 @@ -export 'home/home_account_ready/chat_only.dart'; export 'default_app_bar.dart'; -export 'edit_account.dart'; -export 'edit_contact.dart'; -export 'home.dart'; +export 'home/home.dart'; export 'index.dart'; -export 'main_pager/main_pager.dart'; diff --git a/lib/old_to_refactor/providers/conversation.dart b/lib/old_to_refactor/providers/conversation.dart index 451f8e3..3cce233 100644 --- a/lib/old_to_refactor/providers/conversation.dart +++ b/lib/old_to_refactor/providers/conversation.dart @@ -5,18 +5,13 @@ import 'dart:convert'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; -import 'package:riverpod_annotation/riverpod_annotation.dart'; +import 'package:veilid_support/veilid_support.dart'; +import '../../account_manager/account_manager.dart'; import '../../proto/proto.dart' as proto; - import '../../tools/tools.dart'; -import '../../init.dart'; -import '../../../packages/veilid_support/veilid_support.dart'; -import 'account.dart'; -import 'chat.dart'; -import 'contact.dart'; -part 'conversation.g.dart'; +import 'chat.dart'; class Conversation { Conversation._( diff --git a/packages/veilid_support/lib/dht_support/dht_support.dart b/packages/veilid_support/lib/dht_support/dht_support.dart index 0d56e45..5ac4022 100644 --- a/packages/veilid_support/lib/dht_support/dht_support.dart +++ b/packages/veilid_support/lib/dht_support/dht_support.dart @@ -7,3 +7,4 @@ export 'src/dht_record_crypto.dart'; export 'src/dht_record_cubit.dart'; export 'src/dht_record_pool.dart'; export 'src/dht_short_array.dart'; +export 'src/dht_short_array_cubit.dart'; diff --git a/packages/veilid_support/lib/dht_support/src/dht_short_array_cubit.dart b/packages/veilid_support/lib/dht_support/src/dht_short_array_cubit.dart index 735d79a..179bc04 100644 --- a/packages/veilid_support/lib/dht_support/src/dht_short_array_cubit.dart +++ b/packages/veilid_support/lib/dht_support/src/dht_short_array_cubit.dart @@ -7,12 +7,32 @@ import '../../veilid_support.dart'; class DHTShortArrayCubit extends Cubit>> { DHTShortArrayCubit({ + required Future Function() open, + required T Function(List data) decodeElement, + }) : _decodeElement = decodeElement, + _wantsUpdate = false, + _isUpdating = false, + _wantsCloseRecord = false, + super(const AsyncValue.loading()) { + Future.delayed(Duration.zero, () async { + // Open DHT record + _shortArray = await open(); + _wantsCloseRecord = true; + + // Make initial state update + _update(); + _subscription = await _shortArray.listen(_update); + }); + } + + DHTShortArrayCubit.value({ required DHTShortArray shortArray, required T Function(List data) decodeElement, }) : _shortArray = shortArray, _decodeElement = decodeElement, _wantsUpdate = false, _isUpdating = false, + _wantsCloseRecord = false, super(const AsyncValue.loading()) { // Make initial state update _update(); @@ -65,12 +85,16 @@ class DHTShortArrayCubit extends Cubit>> { Future close() async { await _subscription?.cancel(); _subscription = null; + if (_wantsCloseRecord) { + await _shortArray.close(); + } await super.close(); } - final DHTShortArray _shortArray; + late final DHTShortArray _shortArray; final T Function(List data) _decodeElement; StreamSubscription? _subscription; bool _wantsUpdate; bool _isUpdating; + bool _wantsCloseRecord; } diff --git a/pubspec.lock b/pubspec.lock index 036283a..ee6c0fa 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1014,7 +1014,7 @@ packages: source: hosted version: "3.1.0" provider: - dependency: transitive + dependency: "direct main" description: name: provider sha256: "9a96a0a19b594dbc5bf0f1f27d2bc67d5f95957359b461cd9feb44ed6ae75096" diff --git a/pubspec.yaml b/pubspec.yaml index fc88a54..f91527d 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -58,6 +58,7 @@ dependencies: pinput: ^3.0.1 preload_page_view: ^0.2.0 protobuf: ^3.0.0 + provider: ^6.1.1 qr_code_dart_scan: ^0.7.2 qr_flutter: ^4.1.0 quickalert: ^1.0.1 From 7bd426ce983b12bc028026150ccdc6474d2406fe Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Sun, 28 Jan 2024 21:31:53 -0500 Subject: [PATCH 18/68] more refactor, conversation work --- .../contact_invitation.dart | 3 +- .../cubits/contact_invitation_list_cubit.dart | 84 +++--- lib/contact_invitation/models/models.dart | 2 +- .../models/valid_contact_invitation.dart | 15 +- lib/contacts/contacts.dart | 1 + .../models}/contact.dart | 0 .../models}/conversation.dart | 243 +++++++++--------- lib/contacts/models/models.dart | 1 + lib/layout/home/home.dart | 7 +- .../home_account_ready.dart | 62 ++--- .../main_pager/account_page.dart | 12 +- .../main_pager/main_pager.dart | 36 +-- .../components/account_bubble.dart | 55 ---- .../providers/contact_invite.dart | 56 ---- .../src/dht_short_array_cubit.dart | 17 ++ 15 files changed, 204 insertions(+), 390 deletions(-) rename lib/{old_to_refactor/providers => contacts/models}/contact.dart (100%) rename lib/{old_to_refactor/providers => contacts/models}/conversation.dart (59%) create mode 100644 lib/contacts/models/models.dart delete mode 100644 lib/old_to_refactor/components/account_bubble.dart delete mode 100644 lib/old_to_refactor/providers/contact_invite.dart diff --git a/lib/contact_invitation/contact_invitation.dart b/lib/contact_invitation/contact_invitation.dart index 9ca8bca..08ae2e7 100644 --- a/lib/contact_invitation/contact_invitation.dart +++ b/lib/contact_invitation/contact_invitation.dart @@ -1,2 +1,3 @@ -export 'repository/contact_invitation_repository.dart'; +export 'cubits/cubits.dart'; +export 'models/models.dart'; export 'views/views.dart'; diff --git a/lib/contact_invitation/cubits/contact_invitation_list_cubit.dart b/lib/contact_invitation/cubits/contact_invitation_list_cubit.dart index 1e008de..cc73991 100644 --- a/lib/contact_invitation/cubits/contact_invitation_list_cubit.dart +++ b/lib/contact_invitation/cubits/contact_invitation_list_cubit.dart @@ -1,11 +1,11 @@ import 'dart:async'; -import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:fixnum/fixnum.dart'; import 'package:flutter/foundation.dart'; import 'package:veilid_support/veilid_support.dart'; import '../../account_manager/account_manager.dart'; +import '../../contacts/contacts.dart'; import '../../proto/proto.dart' as proto; import '../../tools/tools.dart'; import '../models/models.dart'; @@ -33,19 +33,19 @@ class InvitationStatus { ////////////////////////////////////////////////// // Mutable state for per-account contact invitations -class ContactInvitationListCubit extends DHTShortArrayCubit { +class ContactInvitationListCubit + extends DHTShortArrayCubit { ContactInvitationListCubit({ required ActiveAccountInfo activeAccountInfo, required proto.Account account, - required DHTShortArray dhtRecord, }) : _activeAccountInfo = activeAccountInfo, _account = account, - _dhtRecord = dhtRecord, - super(shortArray: dhtRecord, decodeElement: proto.ContactInvitation.fromBuffer); -xxx convert the rest of this to cubit - static Future open( - ActiveAccountInfo activeAccountInfo, proto.Account account) async { + super( + open: () => _open(activeAccountInfo, account), + decodeElement: proto.ContactInvitationRecord.fromBuffer); + static Future _open( + ActiveAccountInfo activeAccountInfo, proto.Account account) async { final accountRecordKey = activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey; @@ -57,28 +57,9 @@ xxx convert the rest of this to cubit contactInvitationListRecordKey, parent: accountRecordKey); - return ContactInvitationRepository._( - activeAccountInfo: activeAccountInfo, - account: account, - dhtRecord: dhtRecord); + return dhtRecord; } - @override - Future close() async { - await _dhtRecord.close(); - await super.close(); - } - - // Future refresh() async { - // for (var i = 0; i < _dhtRecord.length; i++) { - // final cir = await _dhtRecord.getItem(i); - // if (cir == null) { - // throw Exception('Failed to get contact invitation record'); - // } - // _records = _records.add(proto.ContactInvitationRecord.fromBuffer(cir)); - // } - // } - Future createInvitation( {required EncryptionKeyType encryptionKeyType, required String encryptionKey, @@ -98,7 +79,8 @@ xxx convert the rest of this to cubit encryptionKey: encryptionKey, ); - // Create local chat DHT record with the account record key as its parent + // Create local conversation 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 @@ -163,7 +145,7 @@ xxx convert the rest of this to cubit // Add ContactInvitationRecord to account's list // if this fails, don't keep retrying, user can try again later - if (await _dhtRecord.tryAddItem(cinvrec.writeToBuffer()) == false) { + if (await shortArray.tryAddItem(cinvrec.writeToBuffer()) == false) { throw Exception('Failed to add contact invitation record'); } }); @@ -180,15 +162,15 @@ xxx convert the rest of this to cubit _activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey; // Remove ContactInvitationRecord from account's list - for (var i = 0; i < _dhtRecord.length; i++) { - final item = await _dhtRecord.getItemProtobuf( + for (var i = 0; i < shortArray.length; i++) { + final item = await shortArray.getItemProtobuf( proto.ContactInvitationRecord.fromBuffer, i); if (item == null) { throw Exception('Failed to get contact invitation record'); } if (item.contactRequestInbox.recordKey == contactInvitationRecord.contactRequestInbox.recordKey) { - await _dhtRecord.tryRemoveItem(i); + await shortArray.tryRemoveItem(i); break; } } @@ -229,11 +211,11 @@ xxx convert the rest of this to cubit final cs = await pool.veilid.getCryptoSystem(contactRequestInboxKey.kind); - // See if we're chatting to ourselves, if so, don't delete it here - final isSelf = _contactIdentityMaster.identityPublicKey == - _activeAccountInfo.localAccount.identityMaster.identityPublicKey; -xxx this doesn't work and the upper one doesnt either - final isSelf = _records.indexWhere((cir) => + // Compare the invitation's contact request + // inbox with our list of extant invitations + // If we're chatting to ourselves, + // we are validating an invitation we have created + final isSelf = state.data!.value.indexWhere((cir) => proto.TypedKeyProto.fromProto(cir.contactRequestInbox.recordKey) == contactRequestInboxKey) != -1; @@ -283,10 +265,7 @@ xxx this doesn't work and the upper one doesnt either out = ValidContactInvitation( activeAccountInfo: _activeAccountInfo, - signedContactInvitation: signedContactInvitation, - contactInvitation: contactInvitation, contactRequestInboxKey: contactRequestInboxKey, - contactRequest: contactRequest, contactRequestPrivate: contactRequestPrivate, contactIdentityMaster: contactIdentityMaster, writer: writer); @@ -296,13 +275,12 @@ xxx this doesn't work and the upper one doesnt either } Future checkInvitationStatus( - {required ActiveAccountInfo activeAccountInfo, - required proto.ContactInvitationRecord contactInvitationRecord}) async { + {required proto.ContactInvitationRecord contactInvitationRecord}) async { // Open the contact request inbox try { final pool = DHTRecordPool.instance; - final accountRecordKey = - activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey; + final accountRecordKey = _activeAccountInfo + .userLogin.accountRecordInfo.accountRecord.recordKey; final writerKey = proto.CryptoKeyProto.fromProto(contactInvitationRecord.writerKey); final writerSecret = @@ -350,11 +328,14 @@ xxx this doesn't work and the upper one doesnt either // Pull profile from remote conversation key final remoteConversationRecordKey = proto.TypedKeyProto.fromProto( contactResponse.remoteConversationRecordKey); - final remoteConversation = await readRemoteConversation( - activeAccountInfo: activeAccountInfo, + + final conversation = ConversationManager( + activeAccountInfo: _activeAccountInfo, remoteIdentityPublicKey: contactIdentityMaster.identityPublicTypedKey(), remoteConversationRecordKey: remoteConversationRecordKey); + + final remoteConversation = await conversation.readRemoteConversation(); if (remoteConversation == null) { log.info('Remote conversation could not be read. Waiting...'); return null; @@ -362,11 +343,9 @@ xxx this doesn't work and the upper one doesnt either // Complete the local conversation now that we have the remote profile final localConversationRecordKey = proto.TypedKeyProto.fromProto( contactInvitationRecord.localConversationRecordKey); - return createConversation( - activeAccountInfo: activeAccountInfo, - remoteIdentityPublicKey: - contactIdentityMaster.identityPublicTypedKey(), + return conversation.initLocalConversation( existingConversationRecordKey: localConversationRecordKey, + profile: xxx LOCAL PROFILE HERE NOT REMOTE // ignore: prefer_expression_function_bodies callback: (localConversation) async { return InvitationStatus( @@ -400,9 +379,6 @@ xxx this doesn't work and the upper one doesnt either } // - final ActiveAccountInfo _activeAccountInfo; final proto.Account _account; - final DHTShortArray _dhtRecord; - //IList _records; } diff --git a/lib/contact_invitation/models/models.dart b/lib/contact_invitation/models/models.dart index 331af2d..0936f63 100644 --- a/lib/contact_invitation/models/models.dart +++ b/lib/contact_invitation/models/models.dart @@ -1,2 +1,2 @@ export 'accepted_contact.dart'; -export '../repository/valid_contact_invitation.dart'; +export 'valid_contact_invitation.dart'; diff --git a/lib/contact_invitation/models/valid_contact_invitation.dart b/lib/contact_invitation/models/valid_contact_invitation.dart index 2af9c30..01c8bb7 100644 --- a/lib/contact_invitation/models/valid_contact_invitation.dart +++ b/lib/contact_invitation/models/valid_contact_invitation.dart @@ -4,8 +4,8 @@ import 'package:veilid_support/veilid_support.dart'; import '../../account_manager/account_manager.dart'; import '../../proto/proto.dart' as proto; import '../../tools/tools.dart'; -import '../models/models.dart'; -import 'contact_invitation_repository.dart'; +import 'models.dart'; +import '../cubits/contact_invitation_list_cubit.dart'; ////////////////////////////////////////////////// /// @@ -14,18 +14,12 @@ class ValidContactInvitation { @internal ValidContactInvitation( {required ActiveAccountInfo activeAccountInfo, - required proto.SignedContactInvitation signedContactInvitation, - required proto.ContactInvitation contactInvitation, required TypedKey contactRequestInboxKey, - required proto.ContactRequest contactRequest, required proto.ContactRequestPrivate contactRequestPrivate, required IdentityMaster contactIdentityMaster, required KeyPair writer}) : _activeAccountInfo = activeAccountInfo, - _signedContactInvitation = signedContactInvitation, - _contactInvitation = contactInvitation, _contactRequestInboxKey = contactRequestInboxKey, - _contactRequest = contactRequest, _contactRequestPrivate = contactRequestPrivate, _contactIdentityMaster = contactIdentityMaster, _writer = writer; @@ -140,8 +134,5 @@ class ValidContactInvitation { final TypedKey _contactRequestInboxKey; final IdentityMaster _contactIdentityMaster; final KeyPair _writer; - proto.SignedContactInvitation _signedContactInvitation; - proto.ContactInvitation _contactInvitation; - proto.ContactRequest _contactRequest; - proto.ContactRequestPrivate _contactRequestPrivate; + final proto.ContactRequestPrivate _contactRequestPrivate; } diff --git a/lib/contacts/contacts.dart b/lib/contacts/contacts.dart index 83d1303..3e9c176 100644 --- a/lib/contacts/contacts.dart +++ b/lib/contacts/contacts.dart @@ -1 +1,2 @@ +export 'models/models.dart'; export 'views/views.dart'; diff --git a/lib/old_to_refactor/providers/contact.dart b/lib/contacts/models/contact.dart similarity index 100% rename from lib/old_to_refactor/providers/contact.dart rename to lib/contacts/models/contact.dart diff --git a/lib/old_to_refactor/providers/conversation.dart b/lib/contacts/models/conversation.dart similarity index 59% rename from lib/old_to_refactor/providers/conversation.dart rename to lib/contacts/models/conversation.dart index 3cce233..4e6d712 100644 --- a/lib/old_to_refactor/providers/conversation.dart +++ b/lib/contacts/models/conversation.dart @@ -2,6 +2,7 @@ // Each Contact in the ContactList has at most one Conversation between the // remote contact and the local account +import 'dart:async'; import 'dart:convert'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; @@ -11,32 +12,86 @@ import '../../account_manager/account_manager.dart'; import '../../proto/proto.dart' as proto; import '../../tools/tools.dart'; -import 'chat.dart'; - -class Conversation { - Conversation._( +class ConversationManager { + ConversationManager( {required ActiveAccountInfo activeAccountInfo, - required TypedKey localConversationRecordKey, required TypedKey remoteIdentityPublicKey, - required TypedKey remoteConversationRecordKey}) + TypedKey? localConversationRecordKey, + TypedKey? remoteConversationRecordKey}) : _activeAccountInfo = activeAccountInfo, _localConversationRecordKey = localConversationRecordKey, _remoteIdentityPublicKey = remoteIdentityPublicKey, _remoteConversationRecordKey = remoteConversationRecordKey; - Future open() async {} + // Initialize a local conversation + // If we were the initiator of the conversation there may be an + // incomplete 'existingConversationRecord' that we need to fill + // in now that we have the remote identity key + Future initLocalConversation( + {required proto.Profile profile, + required FutureOr Function(DHTRecord) callback, + TypedKey? existingConversationRecordKey}) async { + final pool = DHTRecordPool.instance; + final accountRecordKey = + _activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey; - Future close() async { - // + final crypto = await getConversationCrypto(); + final writer = _activeAccountInfo.conversationWriter; + + // Open with SMPL scheme for identity writer + late final DHTRecord localConversationRecord; + if (existingConversationRecordKey != null) { + localConversationRecord = await pool.openWrite( + existingConversationRecordKey, writer, + parent: accountRecordKey, crypto: crypto); + } else { + final localConversationRecordCreate = await pool.create( + parent: accountRecordKey, + crypto: crypto, + schema: DHTSchema.smpl( + oCnt: 0, members: [DHTSchemaMember(mKey: writer.key, mCnt: 1)])); + await localConversationRecordCreate.close(); + localConversationRecord = await pool.openWrite( + localConversationRecordCreate.key, writer, + parent: accountRecordKey, crypto: crypto); + } + final out = localConversationRecord + // ignore: prefer_expression_function_bodies + .deleteScope((localConversation) async { + // Make messages log + return (await DHTShortArray.create( + parent: localConversation.key, + crypto: crypto, + smplWriter: writer)) + .deleteScope((messages) async { + // Write local conversation key + final conversation = proto.Conversation() + ..profile = profile + ..identityMasterJson = jsonEncode( + _activeAccountInfo.localAccount.identityMaster.toJson()) + ..messages = messages.record.key.toProto(); + + // + final update = await localConversation.tryWriteProtobuf( + proto.Conversation.fromBuffer, conversation); + if (update != null) { + throw Exception('Failed to write local conversation'); + } + return await callback(localConversation); + }); + }); + // If success, save the new local conversation record key in this object + _localConversationRecordKey = localConversationRecord.key; + return out; } Future readRemoteConversation() async { final accountRecordKey = _activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey; - final pool = await DHTRecordPool.instance(); + final pool = DHTRecordPool.instance; final crypto = await getConversationCrypto(); - return (await pool.openRead(_remoteConversationRecordKey, + return (await pool.openRead(_remoteConversationRecordKey!, parent: accountRecordKey, crypto: crypto)) .scope((remoteConversation) async { // @@ -49,10 +104,10 @@ class Conversation { Future readLocalConversation() async { final accountRecordKey = _activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey; - final pool = await DHTRecordPool.instance(); + final pool = DHTRecordPool.instance; final crypto = await getConversationCrypto(); - return (await pool.openRead(_localConversationRecordKey, + return (await pool.openRead(_localConversationRecordKey!, parent: accountRecordKey, crypto: crypto)) .scope((localConversation) async { // @@ -70,12 +125,12 @@ class Conversation { }) async { final accountRecordKey = _activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey; - final pool = await DHTRecordPool.instance(); + final pool = DHTRecordPool.instance; final crypto = await getConversationCrypto(); - final writer = _activeAccountInfo.getConversationWriter(); + final writer = _activeAccountInfo.conversationWriter; - return (await pool.openWrite(_localConversationRecordKey, writer, + return (await pool.openWrite(_localConversationRecordKey!, writer, parent: accountRecordKey, crypto: crypto)) .scope((localConversation) async { // @@ -97,7 +152,7 @@ class Conversation { final messagesRecordKey = proto.TypedKeyProto.fromProto(conversation.messages); final crypto = await getConversationCrypto(); - final writer = _activeAccountInfo.getConversationWriter(); + final writer = _activeAccountInfo.conversationWriter; await (await DHTShortArray.openWrite(messagesRecordKey, writer, parent: _localConversationRecordKey, crypto: crypto)) @@ -116,7 +171,7 @@ class Conversation { final messagesRecordKey = proto.TypedKeyProto.fromProto(conversation.messages); final crypto = await getConversationCrypto(); - final writer = _activeAccountInfo.getConversationWriter(); + final writer = _activeAccountInfo.conversationWriter; newMessages = newMessages.sort((a, b) => Timestamp.fromInt64(a.timestamp) .compareTo(Timestamp.fromInt64(b.timestamp))); @@ -195,9 +250,8 @@ class Conversation { if (conversationCrypto != null) { return conversationCrypto; } - final veilid = await eventualVeilid.future; final identitySecret = _activeAccountInfo.userLogin.identitySecret; - final cs = await veilid.getCryptoSystem(identitySecret.kind); + final cs = await Veilid.instance.getCryptoSystem(identitySecret.kind); final sharedSecret = await cs.cachedDH(_remoteIdentityPublicKey.value, identitySecret.value); @@ -232,119 +286,60 @@ class Conversation { } final ActiveAccountInfo _activeAccountInfo; - final TypedKey _localConversationRecordKey; final TypedKey _remoteIdentityPublicKey; - final TypedKey _remoteConversationRecordKey; + TypedKey? _localConversationRecordKey; + TypedKey? _remoteConversationRecordKey; // DHTRecordCrypto? _conversationCrypto; } -// Create a conversation -// If we were the initiator of the conversation there may be an -// incomplete 'existingConversationRecord' that we need to fill -// in now that we have the remote identity key -Future createConversation( - {required ActiveAccountInfo activeAccountInfo, - required TypedKey remoteIdentityPublicKey, - required FutureOr Function(DHTRecord) callback, - TypedKey? existingConversationRecordKey}) async { - final pool = await DHTRecordPool.instance(); - final accountRecordKey = - activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey; - final crypto = await getConversationCrypto( - activeAccountInfo: activeAccountInfo, - remoteIdentityPublicKey: remoteIdentityPublicKey); - final writer = activeAccountInfo.getConversationWriter(); +// // +// // +// // +// // - // Open with SMPL scheme for identity writer - late final DHTRecord localConversationRecord; - if (existingConversationRecordKey != null) { - localConversationRecord = await pool.openWrite( - existingConversationRecordKey, writer, - parent: accountRecordKey, crypto: crypto); - } else { - final localConversationRecordCreate = await pool.create( - parent: accountRecordKey, - crypto: crypto, - schema: DHTSchema.smpl( - oCnt: 0, members: [DHTSchemaMember(mKey: writer.key, mCnt: 1)])); - await localConversationRecordCreate.close(); - localConversationRecord = await pool.openWrite( - localConversationRecordCreate.key, writer, - parent: accountRecordKey, crypto: crypto); - } - return localConversationRecord - // ignore: prefer_expression_function_bodies - .deleteScope((localConversation) async { - // Make messages log - return (await DHTShortArray.create( - parent: localConversation.key, crypto: crypto, smplWriter: writer)) - .deleteScope((messages) async { - // Write local conversation key - final conversation = proto.Conversation() - ..profile = activeAccountInfo.account.profile - ..identityMasterJson = - jsonEncode(activeAccountInfo.localAccount.identityMaster.toJson()) - ..messages = messages.record.key.toProto(); +// @riverpod +// class ActiveConversationMessages extends _$ActiveConversationMessages { +// /// Get message for active conversation +// @override +// FutureOr?> build() async { +// await eventualVeilid.future; - // - final update = await localConversation.tryWriteProtobuf( - proto.Conversation.fromBuffer, conversation); - if (update != null) { - throw Exception('Failed to write local conversation'); - } - return await callback(localConversation); - }); - }); -} +// final activeChat = ref.watch(activeChatStateProvider); +// if (activeChat == null) { +// return null; +// } -// -// -// -// +// final activeAccountInfo = +// await ref.watch(fetchActiveAccountProvider.future); +// if (activeAccountInfo == null) { +// return null; +// } -@riverpod -class ActiveConversationMessages extends _$ActiveConversationMessages { - /// Get message for active conversation - @override - FutureOr?> build() async { - await eventualVeilid.future; +// final contactList = ref.watch(fetchContactListProvider).asData?.value ?? +// const IListConst([]); - final activeChat = ref.watch(activeChatStateProvider); - if (activeChat == null) { - return null; - } +// final activeChatContactIdx = contactList.indexWhere( +// (c) => +// proto.TypedKeyProto.fromProto(c.remoteConversationRecordKey) == +// activeChat, +// ); +// if (activeChatContactIdx == -1) { +// return null; +// } +// final activeChatContact = contactList[activeChatContactIdx]; +// final remoteIdentityPublicKey = +// proto.TypedKeyProto.fromProto(activeChatContact.identityPublicKey); +// // final remoteConversationRecordKey = proto.TypedKeyProto.fromProto( +// // activeChatContact.remoteConversationRecordKey); +// final localConversationRecordKey = proto.TypedKeyProto.fromProto( +// activeChatContact.localConversationRecordKey); - final activeAccountInfo = - await ref.watch(fetchActiveAccountProvider.future); - if (activeAccountInfo == null) { - return null; - } - - final contactList = ref.watch(fetchContactListProvider).asData?.value ?? - const IListConst([]); - - final activeChatContactIdx = contactList.indexWhere( - (c) => - proto.TypedKeyProto.fromProto(c.remoteConversationRecordKey) == - activeChat, - ); - if (activeChatContactIdx == -1) { - return null; - } - final activeChatContact = contactList[activeChatContactIdx]; - final remoteIdentityPublicKey = - proto.TypedKeyProto.fromProto(activeChatContact.identityPublicKey); - // final remoteConversationRecordKey = proto.TypedKeyProto.fromProto( - // activeChatContact.remoteConversationRecordKey); - final localConversationRecordKey = proto.TypedKeyProto.fromProto( - activeChatContact.localConversationRecordKey); - - return await getLocalConversationMessages( - activeAccountInfo: activeAccountInfo, - localConversationRecordKey: localConversationRecordKey, - remoteIdentityPublicKey: remoteIdentityPublicKey, - ); - } -} +// return await getLocalConversationMessages( +// activeAccountInfo: activeAccountInfo, +// localConversationRecordKey: localConversationRecordKey, +// remoteIdentityPublicKey: remoteIdentityPublicKey, +// ); +// } +// } diff --git a/lib/contacts/models/models.dart b/lib/contacts/models/models.dart new file mode 100644 index 0000000..4a9d767 --- /dev/null +++ b/lib/contacts/models/models.dart @@ -0,0 +1 @@ +export 'conversation.dart'; diff --git a/lib/layout/home/home.dart b/lib/layout/home/home.dart index 60ba0d3..308d693 100644 --- a/lib/layout/home/home.dart +++ b/lib/layout/home/home.dart @@ -61,12 +61,7 @@ class HomePageState extends State with TickerProviderStateMixin { return BlocProvider( create: (context) => AccountRecordCubit( record: accountInfo.activeAccountInfo!.accountRecord), - child: context.watch().state.builder( - (context, account) => HomeAccountReady( - localAccounts: localAccounts, - activeUserLogin: activeUserLogin, - activeAccountInfo: accountInfo.activeAccountInfo!, - account: account))); + child: HomeAccountReady()); } } diff --git a/lib/layout/home/home_account_ready/home_account_ready.dart b/lib/layout/home/home_account_ready/home_account_ready.dart index af4f13e..055302b 100644 --- a/lib/layout/home/home_account_ready/home_account_ready.dart +++ b/lib/layout/home/home_account_ready/home_account_ready.dart @@ -1,13 +1,11 @@ import 'dart:async'; import 'package:awesome_extensions/awesome_extensions.dart'; -import 'package:equatable/equatable.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_translate/flutter_translate.dart'; import 'package:go_router/go_router.dart'; -import 'package:veilid_support/veilid_support.dart'; import '../../../account_manager/account_manager.dart'; import '../../../contact_invitation/contact_invitation.dart'; @@ -19,11 +17,13 @@ import 'main_pager/main_pager.dart'; class HomeAccountReady extends StatefulWidget { const HomeAccountReady( {required ActiveAccountInfo activeAccountInfo, - required Account account, + required proto.Account account, super.key}) - : _accountReadyContext = accountReadyContext; + : _activeAccountInfo = activeAccountInfo, + _account = account; - final AccountReadyContext _accountReadyContext; + final ActiveAccountInfo _activeAccountInfo; + final proto.Account _account; @override HomeAccountReadyState createState() => HomeAccountReadyState(); @@ -31,32 +31,10 @@ class HomeAccountReady extends StatefulWidget { class HomeAccountReadyState extends State with TickerProviderStateMixin { - // - ContactInvitationRepository? _contactInvitationRepository; - // @override void initState() { super.initState(); - - // Async initialize repositories for the active user - // xxx: this should not be necessary - // xxx: but RepositoryProvider doesn't call dispose() - Future.delayed(Duration.zero, () async { - // - final cir = await ContactInvitationRepository.open( - widget.activeAccountInfo, widget._accountReadyContext.account); - - setState(() { - _contactInvitationRepository = cir; - }); - }); - } - - @override - void dispose() { - super.dispose(); - _contactInvitationRepository?.dispose(); } Widget buildUnlockAccount( @@ -87,11 +65,11 @@ class HomeAccountReadyState extends State context.go('/home/settings'); }).paddingLTRB(0, 0, 8, 0), ProfileWidget( - name: widget._accountReadyContext.account.profile.name, - pronouns: widget._accountReadyContext.account.profile.pronouns, + name: widget._account.profile.name, + pronouns: widget._account.profile.pronouns, ).expanded(), ]).paddingAll(8), - MainPager().expanded() + const MainPager().expanded() ]); } @@ -129,18 +107,14 @@ class HomeAccountReadyState extends State } @override - Widget build(BuildContext context) { - if (_contactInvitationRepository == null) { - return waitingPage(context); - } - - return RepositoryProvider.value( - value: _contactInvitationRepository, - child: responsiveVisibility( - context: context, - phone: false, - ) - ? buildTablet(context) - : buildPhone(context)); - } + Widget build(BuildContext context) => BlocProvider( + create: (context) => ContactInvitationListCubit( + activeAccountInfo: widget._activeAccountInfo, + account: widget._account), + child: responsiveVisibility( + context: context, + phone: false, + ) + ? buildTablet(context) + : buildPhone(context)); } diff --git a/lib/layout/home/home_account_ready/main_pager/account_page.dart b/lib/layout/home/home_account_ready/main_pager/account_page.dart index 35abe86..e8259e0 100644 --- a/lib/layout/home/home_account_ready/main_pager/account_page.dart +++ b/lib/layout/home/home_account_ready/main_pager/account_page.dart @@ -2,11 +2,11 @@ import 'package:awesome_extensions/awesome_extensions.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_translate/flutter_translate.dart'; import 'package:veilid_support/veilid_support.dart'; -import '../../../../account_manager/account_manager.dart'; -import '../../../../proto/proto.dart' as proto; +import '../../../../contact_invitation/contact_invitation.dart'; import '../../../../theme/theme.dart'; class AccountPage extends StatefulWidget { @@ -39,19 +39,17 @@ class AccountPageState extends State { final textTheme = theme.textTheme; final scale = theme.extension()!; - final records = widget.account.contactInvitationRecords; - final contactInvitationRecordList = - ref.watch(fetchContactInvitationRecordsProvider).asData?.value ?? + context.watch().state.data?.value ?? const IListConst([]); - final contactList = ref.watch(fetchContactListProvider).asData?.value ?? + final contactList = context.watch().state.data?.value ?? const IListConst([]); return SizedBox( child: Column(children: [ if (contactInvitationRecordList.isNotEmpty) ExpansionTile( - tilePadding: EdgeInsets.fromLTRB(8, 0, 8, 0), + tilePadding: const EdgeInsets.fromLTRB(8, 0, 8, 0), backgroundColor: scale.primaryScale.border, collapsedBackgroundColor: scale.primaryScale.border, shape: RoundedRectangleBorder( diff --git a/lib/layout/home/home_account_ready/main_pager/main_pager.dart b/lib/layout/home/home_account_ready/main_pager/main_pager.dart index c872224..ee5709d 100644 --- a/lib/layout/home/home_account_ready/main_pager/main_pager.dart +++ b/lib/layout/home/home_account_ready/main_pager/main_pager.dart @@ -1,7 +1,5 @@ import 'dart:async'; -import 'package:fast_immutable_collections/fast_immutable_collections.dart'; -import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter_animate/flutter_animate.dart'; @@ -9,41 +7,22 @@ import 'package:flutter_translate/flutter_translate.dart'; import 'package:preload_page_view/preload_page_view.dart'; import 'package:stylish_bottom_bar/model/bar_items.dart'; import 'package:stylish_bottom_bar/stylish_bottom_bar.dart'; -import 'package:veilid_support/veilid_support.dart'; -import '../../../../proto/proto.dart' as proto; +import '../../../../contact_invitation/contact_invitation.dart'; +import '../../../../theme/theme.dart'; import '../../../../tools/tools.dart'; -import '../../../account_manager/account_manager.dart'; -import '../../../contact_invitation/contact_invitation.dart'; -import '../../../theme/theme.dart'; import 'account_page.dart'; import 'bottom_sheet_action_button.dart'; import 'chats_page.dart'; class MainPager extends StatefulWidget { - const MainPager( - {required this.localAccounts, - required this.activeUserLogin, - required this.account, - super.key}); - - final IList localAccounts; - final TypedKey activeUserLogin; - final proto.Account account; + const MainPager({super.key}); @override MainPagerState createState() => MainPagerState(); static MainPagerState? of(BuildContext context) => context.findAncestorStateOfType(); - @override - void debugFillProperties(DiagnosticPropertiesBuilder properties) { - super.debugFillProperties(properties); - properties - ..add(IterableProperty('localAccounts', localAccounts)) - ..add(DiagnosticsProperty('activeUserLogin', activeUserLogin)) - ..add(DiagnosticsProperty('account', account)); - } } class MainPagerState extends State with TickerProviderStateMixin { @@ -187,12 +166,9 @@ class MainPagerState extends State with TickerProviderStateMixin { _currentPage = index; }); }, - children: [ - AccountPage( - localAccounts: widget.localAccounts, - activeUserLogin: widget.activeUserLogin, - account: widget.account), - const ChatsPage(), + children: const [ + AccountPage(), + ChatsPage(), ])), // appBar: AppBar( // toolbarHeight: 24, diff --git a/lib/old_to_refactor/components/account_bubble.dart b/lib/old_to_refactor/components/account_bubble.dart deleted file mode 100644 index 57424ae..0000000 --- a/lib/old_to_refactor/components/account_bubble.dart +++ /dev/null @@ -1,55 +0,0 @@ -import 'package:awesome_extensions/awesome_extensions.dart'; -import 'package:circular_profile_avatar/circular_profile_avatar.dart'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:window_manager/window_manager.dart'; - -import '../entities/local_account.dart'; -import '../providers/logins.dart'; - -class AccountBubble extends ConsumerWidget { - const AccountBubble({required this.account, super.key}); - final LocalAccount account; - - @override - Widget build(BuildContext context, WidgetRef ref) { - windowManager.setTitleBarStyle(TitleBarStyle.normal); - final logins = ref.watch(loginsProvider); - - return ConstrainedBox( - constraints: const BoxConstraints(maxHeight: 300), - child: Column(mainAxisAlignment: MainAxisAlignment.center, children: [ - Expanded( - flex: 4, - child: CircularProfileAvatar('', - child: Container(color: Theme.of(context).disabledColor))), - const Expanded(child: Text('Placeholder')) - ])); - } - - @override - void debugFillProperties(DiagnosticPropertiesBuilder properties) { - super.debugFillProperties(properties); - properties.add(DiagnosticsProperty('account', account)); - } -} - -class AddAccountBubble extends ConsumerWidget { - const AddAccountBubble({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - windowManager.setTitleBarStyle(TitleBarStyle.normal); - final logins = ref.watch(loginsProvider); - - return Column(mainAxisAlignment: MainAxisAlignment.center, children: [ - CircularProfileAvatar('', - borderWidth: 4, - borderColor: Theme.of(context).unselectedWidgetColor, - child: Container( - color: Colors.blue, child: const Icon(Icons.add, size: 50))), - const Text('Add Account').paddingLTRB(0, 4, 0, 0) - ]); - } -} diff --git a/lib/old_to_refactor/providers/contact_invite.dart b/lib/old_to_refactor/providers/contact_invite.dart deleted file mode 100644 index 24e792a..0000000 --- a/lib/old_to_refactor/providers/contact_invite.dart +++ /dev/null @@ -1,56 +0,0 @@ -import 'dart:typed_data'; - -import 'package:fast_immutable_collections/fast_immutable_collections.dart'; -import 'package:fixnum/fixnum.dart'; -import 'package:riverpod_annotation/riverpod_annotation.dart'; - -import '../../entities/local_account.dart'; -import '../../proto/proto.dart' as proto; -import '../../tools/tools.dart'; -import '../../../packages/veilid_support/veilid_support.dart'; -import 'account.dart'; -import 'conversation.dart'; - -part 'contact_invite.g.dart'; - -/// 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; - } - final accountRecordKey = - activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey; - - // Decode the contact invitation list from the DHT - IList out = const IListConst([]); - - try { - await (await DHTShortArray.openOwned( - proto.OwnedDHTRecordPointerProto.fromProto( - activeAccountInfo.account.contactInvitationRecords), - parent: accountRecordKey)) - .scope((cirList) async { - for (var i = 0; i < cirList.length; i++) { - final cir = await cirList.getItem(i); - if (cir == null) { - throw Exception('Failed to get contact invitation record'); - } - out = out.add(proto.ContactInvitationRecord.fromBuffer(cir)); - } - }); - } on VeilidAPIExceptionTryAgain catch (_) { - // Try again later - ref.invalidateSelf(); - return null; - } on Exception catch (_) { - // Try again later - ref.invalidateSelf(); - rethrow; - } - - return out; -} diff --git a/packages/veilid_support/lib/dht_support/src/dht_short_array_cubit.dart b/packages/veilid_support/lib/dht_support/src/dht_short_array_cubit.dart index 179bc04..d827500 100644 --- a/packages/veilid_support/lib/dht_support/src/dht_short_array_cubit.dart +++ b/packages/veilid_support/lib/dht_support/src/dht_short_array_cubit.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'package:bloc/bloc.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; +import 'package:meta/meta.dart'; import '../../veilid_support.dart'; @@ -41,6 +42,19 @@ class DHTShortArrayCubit extends Cubit>> { }); } + Future refresh({bool forceRefresh = false}) async { + var out = IList(); + // xxx could be parallelized but we need to watch out for rate limits + for (var i = 0; i < _shortArray.length; i++) { + final cir = await _shortArray.getItem(i, forceRefresh: forceRefresh); + if (cir == null) { + throw Exception('Failed to get short array element'); + } + out = out.add(_decodeElement(cir)); + } + emit(AsyncValue.data(out)); + } + void _update() { // Run at most one background update process _wantsUpdate = true; @@ -91,6 +105,9 @@ class DHTShortArrayCubit extends Cubit>> { await super.close(); } + @protected + DHTShortArray get shortArray => _shortArray; + late final DHTShortArray _shortArray; final T Function(List data) _decodeElement; StreamSubscription? _subscription; From 1e6b9f4a43e4f90f30c34ff60205130523cf0594 Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Mon, 29 Jan 2024 22:38:19 -0500 Subject: [PATCH 19/68] refactor checkpoint --- lib/account_manager/views/profile_widget.dart | 30 +++--- .../cubits/contact_invitation_list_cubit.dart | 5 +- .../models/accepted_contact.dart | 4 +- .../models/valid_contact_invitation.dart | 13 ++- .../views/contact_invitation_display.dart | 96 +++++++++---------- .../views/contact_invitation_item_widget.dart | 46 ++++----- .../views/send_invite_dialog.dart | 7 +- lib/contacts/models/contact.dart | 6 +- lib/layout/home/home.dart | 11 ++- .../home_account_ready.dart | 46 ++++----- .../veilid_support/lib/src/future_cubit.dart | 16 ++++ .../veilid_support/lib/veilid_support.dart | 1 + 12 files changed, 145 insertions(+), 136 deletions(-) create mode 100644 packages/veilid_support/lib/src/future_cubit.dart diff --git a/lib/account_manager/views/profile_widget.dart b/lib/account_manager/views/profile_widget.dart index 1ca56ca..7c0a310 100644 --- a/lib/account_manager/views/profile_widget.dart +++ b/lib/account_manager/views/profile_widget.dart @@ -1,22 +1,25 @@ import 'package:awesome_extensions/awesome_extensions.dart'; -import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import '../../theme/theme.dart'; +import '../../tools/tools.dart'; +import '../cubit/cubit.dart'; class ProfileWidget extends StatelessWidget { const ProfileWidget({ - required this.name, - this.pronouns, super.key, }); - final String name; - final String? pronouns; - @override // ignore: prefer_expression_function_bodies Widget build(BuildContext context) { + final accountData = context.watch().state.data; + if (accountData == null) { + return waitingPage(context); + } + final account = accountData.value; + final theme = Theme.of(context); final scale = theme.extension()!; final textTheme = theme.textTheme; @@ -28,21 +31,14 @@ class ProfileWidget extends StatelessWidget { RoundedRectangleBorder(borderRadius: BorderRadius.circular(16))), child: Column(children: [ Text( - name, + account.profile.name, style: textTheme.headlineSmall, textAlign: TextAlign.left, ).paddingAll(4), - if (pronouns != null && pronouns!.isNotEmpty) - Text(pronouns!, style: textTheme.bodyMedium).paddingLTRB(4, 0, 4, 4), + if (account.profile.pronouns.isNotEmpty) + Text(account.profile.pronouns, style: textTheme.bodyMedium) + .paddingLTRB(4, 0, 4, 4), ]), ); } - - @override - void debugFillProperties(DiagnosticPropertiesBuilder properties) { - super.debugFillProperties(properties); - properties - ..add(StringProperty('name', name)) - ..add(StringProperty('pronouns', pronouns)); - } } diff --git a/lib/contact_invitation/cubits/contact_invitation_list_cubit.dart b/lib/contact_invitation/cubits/contact_invitation_list_cubit.dart index cc73991..60f2e74 100644 --- a/lib/contact_invitation/cubits/contact_invitation_list_cubit.dart +++ b/lib/contact_invitation/cubits/contact_invitation_list_cubit.dart @@ -265,6 +265,7 @@ class ContactInvitationListCubit out = ValidContactInvitation( activeAccountInfo: _activeAccountInfo, + account: _account, contactRequestInboxKey: contactRequestInboxKey, contactRequestPrivate: contactRequestPrivate, contactIdentityMaster: contactIdentityMaster, @@ -345,12 +346,12 @@ class ContactInvitationListCubit contactInvitationRecord.localConversationRecordKey); return conversation.initLocalConversation( existingConversationRecordKey: localConversationRecordKey, - profile: xxx LOCAL PROFILE HERE NOT REMOTE + profile: _account.profile, // ignore: prefer_expression_function_bodies callback: (localConversation) async { return InvitationStatus( acceptedContact: AcceptedContact( - profile: remoteConversation.profile, + remoteProfile: remoteConversation.profile, remoteIdentity: contactIdentityMaster, remoteConversationRecordKey: remoteConversationRecordKey, localConversationRecordKey: localConversationRecordKey)); diff --git a/lib/contact_invitation/models/accepted_contact.dart b/lib/contact_invitation/models/accepted_contact.dart index 3f60811..4623b60 100644 --- a/lib/contact_invitation/models/accepted_contact.dart +++ b/lib/contact_invitation/models/accepted_contact.dart @@ -6,13 +6,13 @@ import '../../proto/proto.dart' as proto; @immutable class AcceptedContact { const AcceptedContact({ - required this.profile, + required this.remoteProfile, required this.remoteIdentity, required this.remoteConversationRecordKey, required this.localConversationRecordKey, }); - final proto.Profile profile; + final proto.Profile remoteProfile; final IdentityMaster remoteIdentity; final TypedKey remoteConversationRecordKey; final TypedKey localConversationRecordKey; diff --git a/lib/contact_invitation/models/valid_contact_invitation.dart b/lib/contact_invitation/models/valid_contact_invitation.dart index 01c8bb7..da6b654 100644 --- a/lib/contact_invitation/models/valid_contact_invitation.dart +++ b/lib/contact_invitation/models/valid_contact_invitation.dart @@ -2,10 +2,10 @@ import 'package:meta/meta.dart'; import 'package:veilid_support/veilid_support.dart'; import '../../account_manager/account_manager.dart'; +import '../../contacts/contacts.dart'; import '../../proto/proto.dart' as proto; import '../../tools/tools.dart'; import 'models.dart'; -import '../cubits/contact_invitation_list_cubit.dart'; ////////////////////////////////////////////////// /// @@ -14,11 +14,13 @@ class ValidContactInvitation { @internal ValidContactInvitation( {required ActiveAccountInfo activeAccountInfo, + required proto.Account account, required TypedKey contactRequestInboxKey, required proto.ContactRequestPrivate contactRequestPrivate, required IdentityMaster contactIdentityMaster, required KeyPair writer}) : _activeAccountInfo = activeAccountInfo, + _account = account, _contactRequestInboxKey = contactRequestInboxKey, _contactRequestPrivate = contactRequestPrivate, _contactIdentityMaster = contactIdentityMaster, @@ -38,10 +40,12 @@ class ValidContactInvitation { .maybeDeleteScope(!isSelf, (contactRequestInbox) async { // Create local conversation key for this // contact and send via contact response - return createConversation( + final conversation = ConversationManager( activeAccountInfo: _activeAccountInfo, remoteIdentityPublicKey: - _contactIdentityMaster.identityPublicTypedKey(), + _contactIdentityMaster.identityPublicTypedKey()); + return conversation.initLocalConversation( + profile: _account.profile, callback: (localConversation) async { final contactResponse = proto.ContactResponse() ..accept = true @@ -72,7 +76,7 @@ class ValidContactInvitation { throw Exception('failed to accept contact invitation'); } return AcceptedContact( - profile: _contactRequestPrivate.profile, + remoteProfile: _contactRequestPrivate.profile, remoteIdentity: _contactIdentityMaster, remoteConversationRecordKey: proto.TypedKeyProto.fromProto( _contactRequestPrivate.chatRecordKey), @@ -131,6 +135,7 @@ class ValidContactInvitation { // final ActiveAccountInfo _activeAccountInfo; + final proto.Account _account; final TypedKey _contactRequestInboxKey; final IdentityMaster _contactIdentityMaster; final KeyPair _writer; diff --git a/lib/contact_invitation/views/contact_invitation_display.dart b/lib/contact_invitation/views/contact_invitation_display.dart index dfd2ebf..7b6fed5 100644 --- a/lib/contact_invitation/views/contact_invitation_display.dart +++ b/lib/contact_invitation/views/contact_invitation_display.dart @@ -7,20 +7,23 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_translate/flutter_translate.dart'; +import 'package:provider/provider.dart'; import 'package:qr_flutter/qr_flutter.dart'; import 'package:veilid_support/veilid_support.dart'; import '../../tools/tools.dart'; +class InvitationGeneratorCubit extends FutureCubit { + InvitationGeneratorCubit(super.fut); +} + class ContactInvitationDisplayDialog extends StatefulWidget { const ContactInvitationDisplayDialog({ - required this.name, required this.message, required this.generator, super.key, }); - final String name; final String message; final FutureOr generator; @@ -32,7 +35,6 @@ class ContactInvitationDisplayDialog extends StatefulWidget { void debugFillProperties(DiagnosticPropertiesBuilder properties) { super.debugFillProperties(properties); properties - ..add(StringProperty('name', name)) ..add(StringProperty('message', message)) ..add(DiagnosticsProperty?>('generator', generator)); } @@ -42,14 +44,10 @@ class ContactInvitationDisplayDialogState extends State { final focusNode = FocusNode(); final formKey = GlobalKey(); - late final AutoDisposeFutureProvider _generateFutureProvider; @override void initState() { super.initState(); - - _generateFutureProvider = - AutoDisposeFutureProvider((ref) async => widget.generator); } @override @@ -76,7 +74,9 @@ class ContactInvitationDisplayDialogState //final scale = theme.extension()!; final textTheme = theme.textTheme; - final signedContactInvitationBytesV = ref.watch(_generateFutureProvider); + final signedContactInvitationBytesV = + context.watch().state; + final cardsize = min(MediaQuery.of(context).size.shortestSide - 48.0, 400); @@ -90,49 +90,43 @@ class ContactInvitationDisplayDialogState maxHeight: cardsize), child: signedContactInvitationBytesV.when( loading: () => buildProgressIndicator(context), - data: (data) { - if (data == null) { - Navigator.of(context).pop(); - return const Text(''); - } - return Form( - key: formKey, - child: Column(children: [ - FittedBox( - child: Text( - translate( - 'send_invite_dialog.contact_invitation'), - style: textTheme.headlineSmall! - .copyWith(color: Colors.black))) - .paddingAll(8), - FittedBox( - child: QrImageView.withQr( - size: 300, - qr: QrCode.fromUint8List( - data: data, - errorCorrectLevel: - QrErrorCorrectLevel.L))) - .expanded(), - Text(widget.message, - softWrap: true, - style: textTheme.labelLarge! - .copyWith(color: Colors.black)) - .paddingAll(8), - ElevatedButton.icon( - icon: const Icon(Icons.copy), - label: Text( - translate('send_invite_dialog.copy_invitation')), - onPressed: () async { - showInfoToast( - context, - translate( - 'send_invite_dialog.invitation_copied')); - await Clipboard.setData(ClipboardData( - text: makeTextInvite(widget.message, data))); - }, - ).paddingAll(16), - ])); - }, + data: (data) => Form( + key: formKey, + child: Column(children: [ + FittedBox( + child: Text( + translate( + 'send_invite_dialog.contact_invitation'), + style: textTheme.headlineSmall! + .copyWith(color: Colors.black))) + .paddingAll(8), + FittedBox( + child: QrImageView.withQr( + size: 300, + qr: QrCode.fromUint8List( + data: data, + errorCorrectLevel: + QrErrorCorrectLevel.L))) + .expanded(), + Text(widget.message, + softWrap: true, + style: textTheme.labelLarge! + .copyWith(color: Colors.black)) + .paddingAll(8), + ElevatedButton.icon( + icon: const Icon(Icons.copy), + label: Text( + translate('send_invite_dialog.copy_invitation')), + onPressed: () async { + showInfoToast( + context, + translate( + 'send_invite_dialog.invitation_copied')); + await Clipboard.setData(ClipboardData( + text: makeTextInvite(widget.message, data))); + }, + ).paddingAll(16), + ])), error: (e, s) { Navigator.of(context).pop(); showErrorToast(context, diff --git a/lib/contact_invitation/views/contact_invitation_item_widget.dart b/lib/contact_invitation/views/contact_invitation_item_widget.dart index 5b5b9ef..cb54d28 100644 --- a/lib/contact_invitation/views/contact_invitation_item_widget.dart +++ b/lib/contact_invitation/views/contact_invitation_item_widget.dart @@ -1,10 +1,11 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_slidable/flutter_slidable.dart'; import 'package:flutter_translate/flutter_translate.dart'; import '../../proto/proto.dart' as proto; import '../../theme/theme.dart'; -import 'contact_invitation_display.dart'; +import '../contact_invitation.dart'; class ContactInvitationItemWidget extends StatelessWidget { const ContactInvitationItemWidget( @@ -48,15 +49,11 @@ class ContactInvitationItemWidget extends StatelessWidget { // A SlidableAction can have an icon and/or a label. SlidableAction( onPressed: (context) async { - final activeAccountInfo = - await ref.read(fetchActiveAccountProvider.future); - if (activeAccountInfo != null) { - await deleteContactInvitation( - accepted: false, - activeAccountInfo: activeAccountInfo, - contactInvitationRecord: contactInvitationRecord); - ref.invalidate(fetchContactInvitationRecordsProvider); - } + final contactInvitationListCubit = + context.read(); + await contactInvitationListCubit.deleteInvitation( + accepted: false, + contactInvitationRecord: contactInvitationRecord); }, backgroundColor: scale.tertiaryScale.background, foregroundColor: scale.tertiaryScale.text, @@ -93,22 +90,21 @@ class ContactInvitationItemWidget extends StatelessWidget { child: ListTile( //title: Text(translate('contact_list.invitation')), onTap: () async { - final activeAccountInfo = - await ref.read(fetchActiveAccountProvider.future); - if (activeAccountInfo != null) { - // ignore: use_build_context_synchronously - if (!context.mounted) { - return; - } - await showDialog( - context: context, - builder: (context) => ContactInvitationDisplayDialog( - name: activeAccountInfo.localAccount.name, - message: contactInvitationRecord.message, - generator: Uint8List.fromList( - contactInvitationRecord.invitation), - )); + // ignore: use_build_context_synchronously + if (!context.mounted) { + return; } + await showDialog( + context: context, + builder: (context) => BlocProvider( + create: (context) => InvitationGeneratorCubit( + Future.value(Uint8List.fromList( + contactInvitationRecord.invitation))), + child: ContactInvitationDisplayDialog( + message: contactInvitationRecord.message, + generator: Uint8List.fromList( + contactInvitationRecord.invitation), + ))); }, title: Text( contactInvitationRecord.message.isEmpty diff --git a/lib/contact_invitation/views/send_invite_dialog.dart b/lib/contact_invitation/views/send_invite_dialog.dart index 3d15d5f..576da32 100644 --- a/lib/contact_invitation/views/send_invite_dialog.dart +++ b/lib/contact_invitation/views/send_invite_dialog.dart @@ -5,6 +5,7 @@ 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_bloc/flutter_bloc.dart'; import 'package:flutter_translate/flutter_translate.dart'; import 'package:veilid_support/veilid_support.dart'; @@ -129,9 +130,11 @@ class SendInviteDialogState extends State { Future _onGenerateButtonPressed() async { final navigator = Navigator.of(context); +xxx continue here + // Start generation - final activeAccountInfo = - await AccountRepository.instance.fetchActiveAccountInfo(); + final activeAccountInfo = context.read(); + if (activeAccountInfo == null) { navigator.pop(); return; diff --git a/lib/contacts/models/contact.dart b/lib/contacts/models/contact.dart index ced5c24..6e91cfe 100644 --- a/lib/contacts/models/contact.dart +++ b/lib/contacts/models/contact.dart @@ -7,10 +7,10 @@ import '../../proto/proto.dart' as proto; import '../../../packages/veilid_support/veilid_support.dart'; import '../../tools/tools.dart'; -import 'account.dart'; -import 'chat.dart'; +import '../../old_to_refactor/providers/account.dart'; +import '../../old_to_refactor/providers/chat.dart'; -part 'contact.g.dart'; +part '../../old_to_refactor/providers/contact.g.dart'; Future createContact({ required ActiveAccountInfo activeAccountInfo, diff --git a/lib/layout/home/home.dart b/lib/layout/home/home.dart index 308d693..8c88f51 100644 --- a/lib/layout/home/home.dart +++ b/lib/layout/home/home.dart @@ -1,6 +1,7 @@ import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:provider/provider.dart'; import 'package:veilid_support/veilid_support.dart'; import '../../account_manager/account_manager.dart'; @@ -58,10 +59,12 @@ class HomePageState extends State with TickerProviderStateMixin { case AccountInfoStatus.accountLocked: return const HomeAccountLocked(); case AccountInfoStatus.accountReady: - return BlocProvider( - create: (context) => AccountRecordCubit( - record: accountInfo.activeAccountInfo!.accountRecord), - child: HomeAccountReady()); + return Provider.value( + value: accountInfo.activeAccountInfo, + child: BlocProvider( + create: (context) => AccountRecordCubit( + record: accountInfo.activeAccountInfo!.accountRecord), + child: HomeAccountReady())); } } diff --git a/lib/layout/home/home_account_ready/home_account_ready.dart b/lib/layout/home/home_account_ready/home_account_ready.dart index 055302b..c4a3eaf 100644 --- a/lib/layout/home/home_account_ready/home_account_ready.dart +++ b/lib/layout/home/home_account_ready/home_account_ready.dart @@ -1,5 +1,3 @@ -import 'dart:async'; - import 'package:awesome_extensions/awesome_extensions.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:flutter/material.dart'; @@ -9,21 +7,12 @@ import 'package:go_router/go_router.dart'; import '../../../account_manager/account_manager.dart'; import '../../../contact_invitation/contact_invitation.dart'; -import '../../../proto/proto.dart' as proto; import '../../../theme/theme.dart'; import '../../../tools/tools.dart'; import 'main_pager/main_pager.dart'; class HomeAccountReady extends StatefulWidget { - const HomeAccountReady( - {required ActiveAccountInfo activeAccountInfo, - required proto.Account account, - super.key}) - : _activeAccountInfo = activeAccountInfo, - _account = account; - - final ActiveAccountInfo _activeAccountInfo; - final proto.Account _account; + const HomeAccountReady({super.key}); @override HomeAccountReadyState createState() => HomeAccountReadyState(); @@ -64,10 +53,7 @@ class HomeAccountReadyState extends State onPressed: () async { context.go('/home/settings'); }).paddingLTRB(0, 0, 8, 0), - ProfileWidget( - name: widget._account.profile.name, - pronouns: widget._account.profile.pronouns, - ).expanded(), + const ProfileWidget().expanded(), ]).paddingAll(8), const MainPager().expanded() ]); @@ -107,14 +93,22 @@ class HomeAccountReadyState extends State } @override - Widget build(BuildContext context) => BlocProvider( - create: (context) => ContactInvitationListCubit( - activeAccountInfo: widget._activeAccountInfo, - account: widget._account), - child: responsiveVisibility( - context: context, - phone: false, - ) - ? buildTablet(context) - : buildPhone(context)); + Widget build(BuildContext context) { + final activeAccountInfo = context.watch(); + final accountData = context.watch().state.data; + + if (accountData == null) { + return waitingPage(context); + } + + return BlocProvider( + create: (context) => ContactInvitationListCubit( + activeAccountInfo: activeAccountInfo, account: accountData.value), + child: responsiveVisibility( + context: context, + phone: false, + ) + ? buildTablet(context) + : buildPhone(context)); + } } diff --git a/packages/veilid_support/lib/src/future_cubit.dart b/packages/veilid_support/lib/src/future_cubit.dart new file mode 100644 index 0000000..851c422 --- /dev/null +++ b/packages/veilid_support/lib/src/future_cubit.dart @@ -0,0 +1,16 @@ +import 'dart:async'; + +import 'package:bloc/bloc.dart'; + +import '../veilid_support.dart'; + +abstract class FutureCubit extends Cubit> { + FutureCubit(Future fut) : super(const AsyncValue.loading()) { + unawaited(fut.then((value) { + emit(AsyncValue.data(value)); + // ignore: avoid_types_on_closure_parameters + }, onError: (Object e, StackTrace stackTrace) { + emit(AsyncValue.error(e, stackTrace)); + })); + } +} diff --git a/packages/veilid_support/lib/veilid_support.dart b/packages/veilid_support/lib/veilid_support.dart index b0dfce0..309f118 100644 --- a/packages/veilid_support/lib/veilid_support.dart +++ b/packages/veilid_support/lib/veilid_support.dart @@ -9,6 +9,7 @@ export 'dht_support/dht_support.dart'; export 'src/async_tag_lock.dart'; export 'src/async_value.dart'; export 'src/config.dart'; +export 'src/future_cubit.dart'; export 'src/identity.dart'; export 'src/json_tools.dart'; export 'src/protobuf_tools.dart'; From 4a8958a868c207b542d2e69bd5ae64582dec7515 Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Tue, 30 Jan 2024 14:14:11 -0500 Subject: [PATCH 20/68] refactor --- .../new_account_page/new_account_page.dart | 1 - lib/account_manager/views/profile_widget.dart | 25 ++-- lib/chat/views/chat_component.dart | 2 +- .../models/valid_contact_invitation.dart | 2 + .../views/invite_dialog.dart | 141 +++++++----------- .../views/send_invite_dialog.dart | 16 +- .../home_account_ready.dart | 55 +++---- 7 files changed, 104 insertions(+), 138 deletions(-) diff --git a/lib/account_manager/views/new_account_page/new_account_page.dart b/lib/account_manager/views/new_account_page/new_account_page.dart index 007bb53..fd8e54a 100644 --- a/lib/account_manager/views/new_account_page/new_account_page.dart +++ b/lib/account_manager/views/new_account_page/new_account_page.dart @@ -10,7 +10,6 @@ import '../../../layout/default_app_bar.dart'; import '../../../tools/tools.dart'; import '../../../veilid_processor/veilid_processor.dart'; import '../../account_manager.dart'; -import '../../models/models.dart'; class NewAccountPage extends StatefulWidget { const NewAccountPage({super.key}); diff --git a/lib/account_manager/views/profile_widget.dart b/lib/account_manager/views/profile_widget.dart index 7c0a310..5d85014 100644 --- a/lib/account_manager/views/profile_widget.dart +++ b/lib/account_manager/views/profile_widget.dart @@ -1,25 +1,24 @@ import 'package:awesome_extensions/awesome_extensions.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; +import '../../proto/proto.dart' as proto; import '../../theme/theme.dart'; -import '../../tools/tools.dart'; -import '../cubit/cubit.dart'; class ProfileWidget extends StatelessWidget { const ProfileWidget({ + required proto.Profile profile, super.key, - }); + }) : _profile = profile; + + // + + final proto.Profile _profile; + + // @override // ignore: prefer_expression_function_bodies Widget build(BuildContext context) { - final accountData = context.watch().state.data; - if (accountData == null) { - return waitingPage(context); - } - final account = accountData.value; - final theme = Theme.of(context); final scale = theme.extension()!; final textTheme = theme.textTheme; @@ -31,12 +30,12 @@ class ProfileWidget extends StatelessWidget { RoundedRectangleBorder(borderRadius: BorderRadius.circular(16))), child: Column(children: [ Text( - account.profile.name, + _profile.name, style: textTheme.headlineSmall, textAlign: TextAlign.left, ).paddingAll(4), - if (account.profile.pronouns.isNotEmpty) - Text(account.profile.pronouns, style: textTheme.bodyMedium) + if (_profile.pronouns.isNotEmpty) + Text(_profile.pronouns, style: textTheme.bodyMedium) .paddingLTRB(4, 0, 4, 4), ]), ); diff --git a/lib/chat/views/chat_component.dart b/lib/chat/views/chat_component.dart index 154f148..323b47c 100644 --- a/lib/chat/views/chat_component.dart +++ b/lib/chat/views/chat_component.dart @@ -10,7 +10,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../old_to_refactor/proto/proto.dart' as proto; import '../../old_to_refactor/providers/account.dart'; import '../../old_to_refactor/providers/chat.dart'; -import '../../old_to_refactor/providers/conversation.dart'; +import '../../contacts/models/conversation.dart'; import '../../old_to_refactor/tools/tools.dart'; import '../../old_to_refactor/veilid_init.dart'; import '../../old_to_refactor/veilid_support/veilid_support.dart'; diff --git a/lib/contact_invitation/models/valid_contact_invitation.dart b/lib/contact_invitation/models/valid_contact_invitation.dart index da6b654..3205c23 100644 --- a/lib/contact_invitation/models/valid_contact_invitation.dart +++ b/lib/contact_invitation/models/valid_contact_invitation.dart @@ -26,6 +26,8 @@ class ValidContactInvitation { _contactIdentityMaster = contactIdentityMaster, _writer = writer; + proto.Profile get remoteProfile => _contactRequestPrivate.profile; + Future accept() async { final pool = DHTRecordPool.instance; try { diff --git a/lib/contact_invitation/views/invite_dialog.dart b/lib/contact_invitation/views/invite_dialog.dart index dc38361..ae24a68 100644 --- a/lib/contact_invitation/views/invite_dialog.dart +++ b/lib/contact_invitation/views/invite_dialog.dart @@ -3,10 +3,12 @@ import 'dart:async'; import 'package:awesome_extensions/awesome_extensions.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_translate/flutter_translate.dart'; import '../../account_manager/account_manager.dart'; import '../../tools/tools.dart'; +import '../contact_invitation.dart'; class InviteDialog extends StatefulWidget { const InviteDialog( @@ -66,22 +68,14 @@ class InviteDialogState extends State { Future _onAccept() async { final navigator = Navigator.of(context); + final activeAccountInfo = context.read(); setState(() { _isAccepting = true; }); - final activeAccountInfo = await ref.read(fetchActiveAccountProvider.future); - if (activeAccountInfo == null) { - setState(() { - _isAccepting = false; - }); - navigator.pop(); - return; - } final validInvitation = _validInvitation; if (validInvitation != null) { - final acceptedContact = - await acceptContactInvitation(activeAccountInfo, validInvitation); + final acceptedContact = await validInvitation.accept(); if (acceptedContact != null) { // initiator when accept is received will create // contact in the case of a 'note to self' @@ -91,7 +85,7 @@ class InviteDialogState extends State { if (!isSelf) { await createContact( activeAccountInfo: activeAccountInfo, - profile: acceptedContact.profile, + profile: acceptedContact.remoteProfile, remoteIdentity: acceptedContact.remoteIdentity, remoteConversationRecordKey: acceptedContact.remoteConversationRecordKey, @@ -99,9 +93,6 @@ class InviteDialogState extends State { acceptedContact.localConversationRecordKey, ); } - ref - ..invalidate(fetchContactInvitationRecordsProvider) - ..invalidate(fetchContactListProvider); } else { if (context.mounted) { showErrorToast(context, 'invite_dialog.failed_to_accept'); @@ -120,17 +111,9 @@ class InviteDialogState extends State { setState(() { _isAccepting = true; }); - final activeAccountInfo = await ref.read(fetchActiveAccountProvider.future); - if (activeAccountInfo == null) { - setState(() { - _isAccepting = false; - }); - navigator.pop(); - return; - } final validInvitation = _validInvitation; if (validInvitation != null) { - if (await rejectContactInvitation(activeAccountInfo, validInvitation)) { + if (await validInvitation.reject()) { // do nothing right now } else { if (context.mounted) { @@ -148,67 +131,56 @@ class InviteDialogState extends State { required Uint8List inviteData, }) async { try { - final activeAccountInfo = - await ref.read(fetchActiveAccountProvider.future); - if (activeAccountInfo == null) { - setState(() { - _isValidating = false; - _validInvitation = null; - }); - return; - } - final contactInvitationRecords = - await ref.read(fetchContactInvitationRecordsProvider.future); + final contactInvitationListCubit = + context.read(); setState(() { _isValidating = true; _validInvitation = null; }); - final validatedContactInvitation = await validateContactInvitation( - activeAccountInfo: activeAccountInfo, - contactInvitationRecords: contactInvitationRecords, - inviteData: inviteData, - getEncryptionKeyCallback: - (cs, encryptionKeyType, encryptedSecret) async { - String encryptionKey; - switch (encryptionKeyType) { - case EncryptionKeyType.none: - encryptionKey = ''; - case EncryptionKeyType.pin: - final description = - translate('invite_dialog.protected_with_pin'); - if (!context.mounted) { - return null; + final validatedContactInvitation = + await contactInvitationListCubit.validateInvitation( + inviteData: inviteData, + getEncryptionKeyCallback: + (cs, encryptionKeyType, encryptedSecret) async { + String encryptionKey; + switch (encryptionKeyType) { + case EncryptionKeyType.none: + encryptionKey = ''; + case EncryptionKeyType.pin: + final description = + translate('invite_dialog.protected_with_pin'); + if (!context.mounted) { + return null; + } + final pin = await showDialog( + context: context, + builder: (context) => EnterPinDialog( + reenter: false, description: description)); + if (pin == null) { + return null; + } + encryptionKey = pin; + case EncryptionKeyType.password: + final description = + translate('invite_dialog.protected_with_password'); + if (!context.mounted) { + return null; + } + final password = await showDialog( + context: context, + builder: (context) => + EnterPasswordDialog(description: description)); + if (password == null) { + return null; + } + encryptionKey = password; } - final pin = await showDialog( - context: context, - builder: (context) => EnterPinDialog( - reenter: false, description: description)); - if (pin == null) { - return null; - } - encryptionKey = pin; - case EncryptionKeyType.password: - final description = - translate('invite_dialog.protected_with_password'); - if (!context.mounted) { - return null; - } - final password = await showDialog( - context: context, - builder: (context) => - EnterPasswordDialog(description: description)); - if (password == null) { - return null; - } - encryptionKey = password; - } - return decryptSecretFromBytes( - secretBytes: encryptedSecret, - cryptoKind: cs.kind(), - encryptionKeyType: encryptionKeyType, - encryptionKey: encryptionKey); - }); + return encryptionKeyType.decryptSecretFromBytes( + secretBytes: encryptedSecret, + cryptoKind: cs.kind(), + encryptionKey: encryptionKey); + }); // Check if validation was cancelled if (validatedContactInvitation == null) { @@ -297,14 +269,11 @@ class InviteDialogState extends State { if (_validInvitation != null && !_isValidating) Column(children: [ Container( - constraints: const BoxConstraints(maxHeight: 64), - width: double.infinity, - child: ProfileWidget( - name: _validInvitation! - .contactRequestPrivate.profile.name, - pronouns: _validInvitation! - .contactRequestPrivate.profile.pronouns, - )).paddingLTRB(0, 0, 0, 8), + constraints: const BoxConstraints(maxHeight: 64), + width: double.infinity, + child: ProfileWidget( + profile: _validInvitation!.remoteProfile)) + .paddingLTRB(0, 0, 0, 8), Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ diff --git a/lib/contact_invitation/views/send_invite_dialog.dart b/lib/contact_invitation/views/send_invite_dialog.dart index 576da32..399ffe8 100644 --- a/lib/contact_invitation/views/send_invite_dialog.dart +++ b/lib/contact_invitation/views/send_invite_dialog.dart @@ -11,7 +11,7 @@ import 'package:veilid_support/veilid_support.dart'; import '../../account_manager/account_manager.dart'; import '../../tools/tools.dart'; -import 'contact_invitation_display.dart'; +import '../contact_invitation.dart'; class SendInviteDialog extends StatefulWidget { const SendInviteDialog({super.key}); @@ -130,17 +130,11 @@ class SendInviteDialogState extends State { Future _onGenerateButtonPressed() async { final navigator = Navigator.of(context); -xxx continue here - // Start generation - final activeAccountInfo = context.read(); + final contactInvitationListCubit = + context.read(); - if (activeAccountInfo == null) { - navigator.pop(); - return; - } - final generator = ContactInvitationRespositoryxxx.createContactInvitation( - activeAccountInfo: activeAccountInfo, + final generator = contactInvitationListCubit.createInvitation( encryptionKeyType: _encryptionKeyType, encryptionKey: _encryptionKey, message: _messageTextController.text, @@ -152,14 +146,12 @@ xxx continue here await showDialog( context: context, builder: (context) => ContactInvitationDisplayDialog( - name: activeAccountInfo.localAccount.name, message: _messageTextController.text, generator: generator, )); // if (ret == null) { // return; // } - ref.invalidate(fetchContactInvitationRecordsProvider); navigator.pop(); } diff --git a/lib/layout/home/home_account_ready/home_account_ready.dart b/lib/layout/home/home_account_ready/home_account_ready.dart index c4a3eaf..110a3a5 100644 --- a/lib/layout/home/home_account_ready/home_account_ready.dart +++ b/lib/layout/home/home_account_ready/home_account_ready.dart @@ -34,37 +34,42 @@ class HomeAccountReadyState extends State return const Center(child: Text('unlock account')); } - Widget buildUserPanel(BuildContext context) { - final theme = Theme.of(context); - final scale = theme.extension()!; + Widget buildUserPanel() => Builder(builder: (context) { + final account = context.watch().state; + final theme = Theme.of(context); + final scale = theme.extension()!; - return Column(children: [ - Row(children: [ - IconButton( - icon: const Icon(Icons.settings), - color: scale.secondaryScale.text, - constraints: const BoxConstraints.expand(height: 64, width: 64), - style: ButtonStyle( - backgroundColor: - MaterialStateProperty.all(scale.secondaryScale.border), - shape: MaterialStateProperty.all(const RoundedRectangleBorder( - borderRadius: BorderRadius.all(Radius.circular(16))))), - tooltip: translate('app_bar.settings_tooltip'), - onPressed: () async { - context.go('/home/settings'); - }).paddingLTRB(0, 0, 8, 0), - const ProfileWidget().expanded(), - ]).paddingAll(8), - const MainPager().expanded() - ]); - } + return Column(children: [ + Row(children: [ + IconButton( + icon: const Icon(Icons.settings), + color: scale.secondaryScale.text, + constraints: const BoxConstraints.expand(height: 64, width: 64), + style: ButtonStyle( + backgroundColor: + MaterialStateProperty.all(scale.secondaryScale.border), + shape: MaterialStateProperty.all( + const RoundedRectangleBorder( + borderRadius: + BorderRadius.all(Radius.circular(16))))), + tooltip: translate('app_bar.settings_tooltip'), + onPressed: () async { + context.go('/home/settings'); + }).paddingLTRB(0, 0, 8, 0), + asyncValueBuilder(account, + (_, account) => ProfileWidget(profile: account.profile)) + .expanded(), + ]).paddingAll(8), + const MainPager().expanded() + ]); + }); Widget buildPhone(BuildContext context) => - Material(color: Colors.transparent, child: buildUserPanel(context)); + Material(color: Colors.transparent, child: buildUserPanel()); Widget buildTabletLeftPane(BuildContext context) => Builder( builder: (context) => - Material(color: Colors.transparent, child: buildUserPanel(context))); + Material(color: Colors.transparent, child: buildUserPanel())); Widget buildTabletRightPane(BuildContext context) => buildChatComponent(); From 03a6a781a689d528206df85333f2862253520669 Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Tue, 30 Jan 2024 17:03:14 -0500 Subject: [PATCH 21/68] chat refactor --- lib/chat/chat.dart | 1 + lib/chat/cubits/active_chat_cubit.dart | 10 ++ lib/chat/cubits/cubits.dart | 1 + lib/chat_list/chat_list.dart | 2 + lib/chat_list/cubits/chat_list_cubit.dart | 72 ++++++++++ lib/chat_list/cubits/cubits.dart | 1 + .../chat_single_contact_item_widget.dart | 18 +-- lib/contacts/contacts.dart | 1 + lib/contacts/cubits/contact_list_cubit.dart | 104 ++++++++++++++ lib/contacts/cubits/cubits.dart | 1 + lib/contacts/models/contact.dart | 132 ------------------ lib/contacts/views/contact_item_widget.dart | 22 +-- lib/contacts/views/contact_list_widget.dart | 10 +- lib/layout/home/home.dart | 7 +- .../home_account_ready.dart | 17 ++- .../main_pager/account_page.dart | 3 +- .../main_pager/chats_page.dart | 52 +------ lib/old_to_refactor/providers/chat.dart | 118 ---------------- 18 files changed, 239 insertions(+), 333 deletions(-) create mode 100644 lib/chat/cubits/active_chat_cubit.dart create mode 100644 lib/chat/cubits/cubits.dart create mode 100644 lib/chat_list/cubits/chat_list_cubit.dart create mode 100644 lib/chat_list/cubits/cubits.dart create mode 100644 lib/contacts/cubits/contact_list_cubit.dart create mode 100644 lib/contacts/cubits/cubits.dart delete mode 100644 lib/contacts/models/contact.dart delete mode 100644 lib/old_to_refactor/providers/chat.dart diff --git a/lib/chat/chat.dart b/lib/chat/chat.dart index 83d1303..6acdd43 100644 --- a/lib/chat/chat.dart +++ b/lib/chat/chat.dart @@ -1 +1,2 @@ +export 'cubits/cubits.dart'; export 'views/views.dart'; diff --git a/lib/chat/cubits/active_chat_cubit.dart b/lib/chat/cubits/active_chat_cubit.dart new file mode 100644 index 0000000..d8cdc3e --- /dev/null +++ b/lib/chat/cubits/active_chat_cubit.dart @@ -0,0 +1,10 @@ +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:veilid_support/veilid_support.dart'; + +class ActiveChatCubit extends Cubit { + ActiveChatCubit(super.initialState); + + void setActiveChat(TypedKey? activeChat) { + emit(activeChat); + } +} diff --git a/lib/chat/cubits/cubits.dart b/lib/chat/cubits/cubits.dart new file mode 100644 index 0000000..20763ea --- /dev/null +++ b/lib/chat/cubits/cubits.dart @@ -0,0 +1 @@ +export 'active_chat_cubit.dart'; diff --git a/lib/chat_list/chat_list.dart b/lib/chat_list/chat_list.dart index e69de29..6acdd43 100644 --- a/lib/chat_list/chat_list.dart +++ b/lib/chat_list/chat_list.dart @@ -0,0 +1,2 @@ +export 'cubits/cubits.dart'; +export 'views/views.dart'; diff --git a/lib/chat_list/cubits/chat_list_cubit.dart b/lib/chat_list/cubits/chat_list_cubit.dart new file mode 100644 index 0000000..bd9b9c5 --- /dev/null +++ b/lib/chat_list/cubits/chat_list_cubit.dart @@ -0,0 +1,72 @@ +import 'dart:async'; + +import 'package:veilid_support/veilid_support.dart'; + +import '../../account_manager/account_manager.dart'; +import '../../proto/proto.dart' as proto; + +////////////////////////////////////////////////// + +////////////////////////////////////////////////// +// Mutable state for per-account chat list + +class ChatListCubit extends DHTShortArrayCubit { + ChatListCubit({ + required ActiveAccountInfo activeAccountInfo, + required proto.Account account, + }) : super( + open: () => _open(activeAccountInfo, account), + decodeElement: proto.Chat.fromBuffer); + + static Future _open( + ActiveAccountInfo activeAccountInfo, proto.Account account) async { + final accountRecordKey = + activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey; + + final chatListRecordKey = + proto.OwnedDHTRecordPointerProto.fromProto(account.chatList); + + final dhtRecord = await DHTShortArray.openOwned(chatListRecordKey, + parent: accountRecordKey); + + return dhtRecord; + } + + /// Create a new chat (singleton for single contact chats) + Future getOrCreateChatSingleContact({ + required TypedKey remoteConversationRecordKey, + }) async { + // Create conversation type Chat + final chat = proto.Chat() + ..type = proto.ChatType.SINGLE_CONTACT + ..remoteConversationKey = remoteConversationRecordKey.toProto(); + + // Add Chat to account's list + // if this fails, don't keep retrying, user can try again later + if (await shortArray.tryAddItem(chat.writeToBuffer()) == false) { + throw Exception('Failed to add chat'); + } + } + + /// Delete a chat + Future deleteChat( + {required TypedKey remoteConversationRecordKey}) async { + // Create conversation type Chat + final remoteConversationKey = remoteConversationRecordKey.toProto(); + + // Remove Chat from account's list + // if this fails, don't keep retrying, user can try again later + + for (var i = 0; i < shortArray.length; i++) { + final cbuf = await shortArray.getItem(i); + if (cbuf == null) { + throw Exception('Failed to get chat'); + } + final c = proto.Chat.fromBuffer(cbuf); + if (c.remoteConversationKey == remoteConversationKey) { + await shortArray.tryRemoveItem(i); + return; + } + } + } +} diff --git a/lib/chat_list/cubits/cubits.dart b/lib/chat_list/cubits/cubits.dart new file mode 100644 index 0000000..cafafff --- /dev/null +++ b/lib/chat_list/cubits/cubits.dart @@ -0,0 +1 @@ +export 'chat_list_cubit.dart'; diff --git a/lib/chat_list/views/chat_single_contact_item_widget.dart b/lib/chat_list/views/chat_single_contact_item_widget.dart index 660b415..3632003 100644 --- a/lib/chat_list/views/chat_single_contact_item_widget.dart +++ b/lib/chat_list/views/chat_single_contact_item_widget.dart @@ -1,21 +1,21 @@ 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 '../proto/proto.dart' as proto; -import '../providers/account.dart'; -import '../providers/chat.dart'; -import '../theme/theme.dart'; +import '../../proto/proto.dart' as proto; +import '../../theme/theme.dart'; -class ChatSingleContactItemWidget extends ConsumerWidget { - const ChatSingleContactItemWidget({required this.contact, super.key}); +class ChatSingleContactItemWidget extends StatelessWidget { + const ChatSingleContactItemWidget({required proto.Contact contact, super.key}) + : _contact = contact; - final proto.Contact contact; + final proto.Contact _contact; @override // ignore: prefer_expression_function_bodies - Widget build(BuildContext context, WidgetRef ref) { + Widget build( + BuildContext context, + ) { final theme = Theme.of(context); //final textTheme = theme.textTheme; final scale = theme.extension()!; diff --git a/lib/contacts/contacts.dart b/lib/contacts/contacts.dart index 3e9c176..08ae2e7 100644 --- a/lib/contacts/contacts.dart +++ b/lib/contacts/contacts.dart @@ -1,2 +1,3 @@ +export 'cubits/cubits.dart'; export 'models/models.dart'; export 'views/views.dart'; diff --git a/lib/contacts/cubits/contact_list_cubit.dart b/lib/contacts/cubits/contact_list_cubit.dart new file mode 100644 index 0000000..004dd51 --- /dev/null +++ b/lib/contacts/cubits/contact_list_cubit.dart @@ -0,0 +1,104 @@ +import 'dart:async'; +import 'dart:convert'; + +import 'package:veilid_support/veilid_support.dart'; + +import '../../account_manager/account_manager.dart'; +import '../../proto/proto.dart' as proto; +import '../../tools/tools.dart'; + +////////////////////////////////////////////////// +// Mutable state for per-account contacts + +class ContactListCubit extends DHTShortArrayCubit { + ContactListCubit({ + required ActiveAccountInfo activeAccountInfo, + required proto.Account account, + }) : _activeAccountInfo = activeAccountInfo, + super( + open: () => _open(activeAccountInfo, account), + decodeElement: proto.Contact.fromBuffer); + + static Future _open( + ActiveAccountInfo activeAccountInfo, proto.Account account) async { + final accountRecordKey = + activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey; + + final contactListRecordKey = + proto.OwnedDHTRecordPointerProto.fromProto(account.contactList); + + final dhtRecord = await DHTShortArray.openOwned(contactListRecordKey, + parent: accountRecordKey); + + return dhtRecord; + } + + Future createContact({ + required proto.Profile remoteProfile, + required IdentityMaster remoteIdentity, + required TypedKey remoteConversationRecordKey, + required TypedKey localConversationRecordKey, + }) async { + // Create Contact + final contact = proto.Contact() + ..editedProfile = remoteProfile + ..remoteProfile = remoteProfile + ..identityMasterJson = jsonEncode(remoteIdentity.toJson()) + ..identityPublicKey = TypedKey( + kind: remoteIdentity.identityRecordKey.kind, + value: remoteIdentity.identityPublicKey) + .toProto() + ..remoteConversationRecordKey = remoteConversationRecordKey.toProto() + ..localConversationRecordKey = localConversationRecordKey.toProto() + ..showAvailability = false; + + // Add Contact to account's list + // if this fails, don't keep retrying, user can try again later + if (await shortArray.tryAddItem(contact.writeToBuffer()) == false) { + throw Exception('Failed to add contact record'); + } + } + + Future deleteContact({required proto.Contact contact}) async { + final pool = DHTRecordPool.instance; + final accountRecordKey = + _activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey; + final localConversationKey = + proto.TypedKeyProto.fromProto(contact.localConversationRecordKey); + final remoteConversationKey = + proto.TypedKeyProto.fromProto(contact.remoteConversationRecordKey); + + // Remove Contact from account's list + for (var i = 0; i < shortArray.length; i++) { + final item = + await shortArray.getItemProtobuf(proto.Contact.fromBuffer, i); + if (item == null) { + throw Exception('Failed to get contact'); + } + if (item.remoteConversationRecordKey == + contact.remoteConversationRecordKey) { + await shortArray.tryRemoveItem(i); + break; + } + } + try { + await (await pool.openRead(localConversationKey, + parent: accountRecordKey)) + .delete(); + } on Exception catch (e) { + log.debug('error removing local conversation record key: $e', e); + } + try { + if (localConversationKey != remoteConversationKey) { + await (await pool.openRead(remoteConversationKey, + parent: accountRecordKey)) + .delete(); + } + } on Exception catch (e) { + log.debug('error removing remote conversation record key: $e', e); + } + } + + // + final ActiveAccountInfo _activeAccountInfo; +} diff --git a/lib/contacts/cubits/cubits.dart b/lib/contacts/cubits/cubits.dart new file mode 100644 index 0000000..795d497 --- /dev/null +++ b/lib/contacts/cubits/cubits.dart @@ -0,0 +1 @@ +export 'contact_list_cubit.dart'; diff --git a/lib/contacts/models/contact.dart b/lib/contacts/models/contact.dart deleted file mode 100644 index 6e91cfe..0000000 --- a/lib/contacts/models/contact.dart +++ /dev/null @@ -1,132 +0,0 @@ -import 'dart:convert'; - -import 'package:fast_immutable_collections/fast_immutable_collections.dart'; -import 'package:riverpod_annotation/riverpod_annotation.dart'; - -import '../../proto/proto.dart' as proto; - -import '../../../packages/veilid_support/veilid_support.dart'; -import '../../tools/tools.dart'; -import '../../old_to_refactor/providers/account.dart'; -import '../../old_to_refactor/providers/chat.dart'; - -part '../../old_to_refactor/providers/contact.g.dart'; - -Future createContact({ - required ActiveAccountInfo activeAccountInfo, - required proto.Profile profile, - required IdentityMaster remoteIdentity, - required TypedKey remoteConversationRecordKey, - required TypedKey localConversationRecordKey, -}) async { - final accountRecordKey = - activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey; - - // Create Contact - final contact = proto.Contact() - ..editedProfile = profile - ..remoteProfile = profile - ..identityMasterJson = jsonEncode(remoteIdentity.toJson()) - ..identityPublicKey = TypedKey( - kind: remoteIdentity.identityRecordKey.kind, - value: remoteIdentity.identityPublicKey) - .toProto() - ..remoteConversationRecordKey = remoteConversationRecordKey.toProto() - ..localConversationRecordKey = localConversationRecordKey.toProto() - ..showAvailability = false; - - // Add Contact 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.contactList), - parent: accountRecordKey)) - .scope((contactList) async { - if (await contactList.tryAddItem(contact.writeToBuffer()) == false) { - throw Exception('Failed to add contact'); - } - }); -} - -Future deleteContact( - {required ActiveAccountInfo activeAccountInfo, - required proto.Contact contact}) async { - final pool = await DHTRecordPool.instance(); - final accountRecordKey = - activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey; - final localConversationKey = - proto.TypedKeyProto.fromProto(contact.localConversationRecordKey); - final remoteConversationKey = - proto.TypedKeyProto.fromProto(contact.remoteConversationRecordKey); - - // Remove any chats for this contact - await deleteChat( - activeAccountInfo: activeAccountInfo, - remoteConversationRecordKey: remoteConversationKey); - - // Remove Contact from account's list - await (await DHTShortArray.openOwned( - proto.OwnedDHTRecordPointerProto.fromProto( - activeAccountInfo.account.contactList), - parent: accountRecordKey)) - .scope((contactList) async { - for (var i = 0; i < contactList.length; i++) { - final item = - await contactList.getItemProtobuf(proto.Contact.fromBuffer, i); - if (item == null) { - throw Exception('Failed to get contact'); - } - if (item.remoteConversationRecordKey == - contact.remoteConversationRecordKey) { - await contactList.tryRemoveItem(i); - break; - } - } - try { - await (await pool.openRead(localConversationKey, - parent: accountRecordKey)) - .delete(); - } on Exception catch (e) { - log.debug('error removing local conversation record key: $e', e); - } - try { - if (localConversationKey != remoteConversationKey) { - await (await pool.openRead(remoteConversationKey, - parent: accountRecordKey)) - .delete(); - } - } on Exception catch (e) { - log.debug('error removing remote conversation record key: $e', e); - } - }); -} - -/// Get the active account contact list -@riverpod -Future?> fetchContactList(FetchContactListRef ref) async { - // See if we've logged into this account or if it is locked - final activeAccountInfo = await ref.watch(fetchActiveAccountProvider.future); - if (activeAccountInfo == null) { - return null; - } - final accountRecordKey = - activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey; - - // Decode the contact list from the DHT - IList out = const IListConst([]); - await (await DHTShortArray.openOwned( - proto.OwnedDHTRecordPointerProto.fromProto( - activeAccountInfo.account.contactList), - parent: accountRecordKey)) - .scope((cList) async { - for (var i = 0; i < cList.length; i++) { - final cir = await cList.getItem(i); - if (cir == null) { - throw Exception('Failed to get contact'); - } - out = out.add(proto.Contact.fromBuffer(cir)); - } - }); - - return out; -} diff --git a/lib/contacts/views/contact_item_widget.dart b/lib/contacts/views/contact_item_widget.dart index 5ffaaa4..0d31d80 100644 --- a/lib/contacts/views/contact_item_widget.dart +++ b/lib/contacts/views/contact_item_widget.dart @@ -1,10 +1,13 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_animate/flutter_animate.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_slidable/flutter_slidable.dart'; import 'package:flutter_translate/flutter_translate.dart'; +import '../../chat_list/chat_list.dart'; import '../../proto/proto.dart' as proto; import '../../theme/theme.dart'; +import '../contacts.dart'; class ContactItemWidget extends StatelessWidget { const ContactItemWidget({required this.contact, super.key}); @@ -38,16 +41,15 @@ class ContactItemWidget extends StatelessWidget { children: [ SlidableAction( onPressed: (context) async { - final activeAccountInfo = - await ref.read(fetchActiveAccountProvider.future); - if (activeAccountInfo != null) { - await deleteContact( - activeAccountInfo: activeAccountInfo, - contact: contact); - ref - ..invalidate(fetchContactListProvider) - ..invalidate(fetchChatListProvider); - } + final contactListCubit = context.read(); + final chatListCubit = context.read(); + + // Remove any chats for this contact + await chatListCubit.deleteChat( + remoteConversationRecordKey: remoteConversationKey); + + // Delete the contact itself + await contactListCubit.deleteContact(contact: contact); }, backgroundColor: scale.tertiaryScale.background, foregroundColor: scale.tertiaryScale.text, diff --git a/lib/contacts/views/contact_list_widget.dart b/lib/contacts/views/contact_list_widget.dart index 1a8c87c..12a4d0a 100644 --- a/lib/contacts/views/contact_list_widget.dart +++ b/lib/contacts/views/contact_list_widget.dart @@ -2,16 +2,16 @@ import 'package:awesome_extensions/awesome_extensions.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_translate/flutter_translate.dart'; import 'package:searchable_listview/searchable_listview.dart'; -import '../proto/proto.dart' as proto; -import '../tools/tools.dart'; +import '../../proto/proto.dart' as proto; +import '../../theme/theme.dart'; +import '../../tools/tools.dart'; import 'contact_item_widget.dart'; import 'empty_contact_list_widget.dart'; -class ContactListWidget extends ConsumerWidget { +class ContactListWidget extends StatelessWidget { const ContactListWidget({required this.contactList, super.key}); final IList contactList; @@ -22,7 +22,7 @@ class ContactListWidget extends ConsumerWidget { } @override - Widget build(BuildContext context, WidgetRef ref) { + Widget build(BuildContext context) { final theme = Theme.of(context); //final textTheme = theme.textTheme; final scale = theme.extension()!; diff --git a/lib/layout/home/home.dart b/lib/layout/home/home.dart index 8c88f51..f7adeaf 100644 --- a/lib/layout/home/home.dart +++ b/lib/layout/home/home.dart @@ -1,8 +1,6 @@ -import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:provider/provider.dart'; -import 'package:veilid_support/veilid_support.dart'; import '../../account_manager/account_manager.dart'; import '../../theme/theme.dart'; @@ -39,8 +37,7 @@ class HomePageState extends State with TickerProviderStateMixin { super.dispose(); } - Widget buildWithLogin(BuildContext context, IList localAccounts, - Typed? activeUserLogin) { + Widget buildWithLogin(BuildContext context) { final activeUserLogin = context.watch().state; if (activeUserLogin == null) { @@ -64,7 +61,7 @@ class HomePageState extends State with TickerProviderStateMixin { child: BlocProvider( create: (context) => AccountRecordCubit( record: accountInfo.activeAccountInfo!.accountRecord), - child: HomeAccountReady())); + child: const HomeAccountReady())); } } diff --git a/lib/layout/home/home_account_ready/home_account_ready.dart b/lib/layout/home/home_account_ready/home_account_ready.dart index 110a3a5..1d900b3 100644 --- a/lib/layout/home/home_account_ready/home_account_ready.dart +++ b/lib/layout/home/home_account_ready/home_account_ready.dart @@ -6,6 +6,8 @@ import 'package:flutter_translate/flutter_translate.dart'; import 'package:go_router/go_router.dart'; import '../../../account_manager/account_manager.dart'; +import '../../../chat/chat.dart'; +import '../../../chat_list/chat_list.dart'; import '../../../contact_invitation/contact_invitation.dart'; import '../../../theme/theme.dart'; import '../../../tools/tools.dart'; @@ -106,9 +108,18 @@ class HomeAccountReadyState extends State return waitingPage(context); } - return BlocProvider( - create: (context) => ContactInvitationListCubit( - activeAccountInfo: activeAccountInfo, account: accountData.value), + return MultiBlocProvider( + providers: [ + BlocProvider( + create: (context) => ContactInvitationListCubit( + activeAccountInfo: activeAccountInfo, + account: accountData.value)), + BlocProvider( + create: (context) => ChatListCubit( + activeAccountInfo: activeAccountInfo, + account: accountData.value)), + BlocProvider(create: (context) => ActiveChatCubit(null)) + ], child: responsiveVisibility( context: context, phone: false, diff --git a/lib/layout/home/home_account_ready/main_pager/account_page.dart b/lib/layout/home/home_account_ready/main_pager/account_page.dart index e8259e0..49a5a49 100644 --- a/lib/layout/home/home_account_ready/main_pager/account_page.dart +++ b/lib/layout/home/home_account_ready/main_pager/account_page.dart @@ -1,12 +1,11 @@ import 'package:awesome_extensions/awesome_extensions.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; -import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_translate/flutter_translate.dart'; -import 'package:veilid_support/veilid_support.dart'; import '../../../../contact_invitation/contact_invitation.dart'; +import '../../../../contacts/contacts.dart'; import '../../../../theme/theme.dart'; class AccountPage extends StatefulWidget { diff --git a/lib/layout/home/home_account_ready/main_pager/chats_page.dart b/lib/layout/home/home_account_ready/main_pager/chats_page.dart index e227e8b..4b663fd 100644 --- a/lib/layout/home/home_account_ready/main_pager/chats_page.dart +++ b/lib/layout/home/home_account_ready/main_pager/chats_page.dart @@ -27,14 +27,9 @@ class ChatsPageState extends State { super.dispose(); } - /// We have an active, unlocked, user login - Widget buildChatList( - BuildContext context, - IList localAccounts, - TypedKey activeUserLogin, - proto.Account account, - // ignore: prefer_expression_function_bodies - ) { + @override + // ignore: prefer_expression_function_bodies + Widget build(BuildContext context) { final contactList = ref.watch(fetchContactListProvider).asData?.value ?? const IListConst([]); final chatList = @@ -47,45 +42,4 @@ class ChatsPageState extends State { .expanded(), if (chatList.isEmpty) const EmptyChatListWidget().expanded(), ]); - } - - @override - // ignore: prefer_expression_function_bodies - Widget build(BuildContext context) { - final localAccountsV = ref.watch(localAccountsProvider); - final loginsV = ref.watch(loginsProvider); - - if (!localAccountsV.hasValue || !loginsV.hasValue) { - return waitingPage(context); - } - final localAccounts = localAccountsV.requireValue; - final logins = loginsV.requireValue; - - final activeUserLogin = logins.activeUserLogin; - if (activeUserLogin == null) { - // If no logged in user is active show a placeholder - return waitingPage(context); - } - final accountV = ref - .watch(fetchAccountProvider(accountMasterRecordKey: activeUserLogin)); - if (!accountV.hasValue) { - return waitingPage(context); - } - final account = accountV.requireValue; - switch (account.status) { - case AccountInfoStatus.noAccount: - return waitingPage(context); - case AccountInfoStatus.accountInvalid: - return waitingPage(context); - case AccountInfoStatus.accountLocked: - return waitingPage(context); - case AccountInfoStatus.accountReady: - return buildChatList( - context, - localAccounts, - activeUserLogin, - account.account!, - ); - } - } } diff --git a/lib/old_to_refactor/providers/chat.dart b/lib/old_to_refactor/providers/chat.dart deleted file mode 100644 index fa4a011..0000000 --- a/lib/old_to_refactor/providers/chat.dart +++ /dev/null @@ -1,118 +0,0 @@ -import 'package:fast_immutable_collections/fast_immutable_collections.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:riverpod_annotation/riverpod_annotation.dart'; - -import '../../proto/proto.dart' as proto; - -import '../../../packages/veilid_support/veilid_support.dart'; -import 'account.dart'; - -part 'chat.g.dart'; - -/// Create a new chat (singleton for single contact chats) -Future getOrCreateChatSingleContact({ - required ActiveAccountInfo activeAccountInfo, - required TypedKey remoteConversationRecordKey, -}) async { - final accountRecordKey = - activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey; - - // Create conversation type Chat - final chat = proto.Chat() - ..type = proto.ChatType.SINGLE_CONTACT - ..remoteConversationKey = remoteConversationRecordKey.toProto(); - - // Add Chat 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.chatList), - parent: accountRecordKey)) - .scope((chatList) async { - for (var i = 0; i < chatList.length; i++) { - final cbuf = await chatList.getItem(i); - if (cbuf == null) { - throw Exception('Failed to get chat'); - } - final c = proto.Chat.fromBuffer(cbuf); - if (c == chat) { - return; - } - } - if (await chatList.tryAddItem(chat.writeToBuffer()) == false) { - throw Exception('Failed to add chat'); - } - }); -} - -/// Delete a chat -Future deleteChat( - {required ActiveAccountInfo activeAccountInfo, - required TypedKey remoteConversationRecordKey}) async { - final accountRecordKey = - activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey; - - // Create conversation type Chat - final remoteConversationKey = remoteConversationRecordKey.toProto(); - - // Add Chat 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.chatList), - parent: accountRecordKey)) - .scope((chatList) async { - for (var i = 0; i < chatList.length; i++) { - final cbuf = await chatList.getItem(i); - if (cbuf == null) { - throw Exception('Failed to get chat'); - } - final c = proto.Chat.fromBuffer(cbuf); - if (c.remoteConversationKey == remoteConversationKey) { - await chatList.tryRemoveItem(i); - - if (activeChatState.state == remoteConversationRecordKey) { - activeChatState.state = null; - } - - return; - } - } - }); -} - -/// Get the active account contact list -@riverpod -Future?> fetchChatList(FetchChatListRef ref) async { - // See if we've logged into this account or if it is locked - final activeAccountInfo = await ref.watch(fetchActiveAccountProvider.future); - if (activeAccountInfo == null) { - return null; - } - final accountRecordKey = - activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey; - - // Decode the chat list from the DHT - IList out = const IListConst([]); - await (await DHTShortArray.openOwned( - proto.OwnedDHTRecordPointerProto.fromProto( - activeAccountInfo.account.chatList), - parent: accountRecordKey)) - .scope((cList) async { - for (var i = 0; i < cList.length; i++) { - final cir = await cList.getItem(i); - if (cir == null) { - throw Exception('Failed to get chat'); - } - out = out.add(proto.Chat.fromBuffer(cir)); - } - }); - - return out; -} - -// The selected chat -final activeChatState = StateController(null); -final activeChatStateProvider = - StateNotifierProvider, TypedKey?>( - (ref) => activeChatState); From ba73123702c4837825390269efcceed21b3c99d4 Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Tue, 30 Jan 2024 18:32:56 -0500 Subject: [PATCH 22/68] chat list work --- .../chat_single_contact_item_widget.dart | 38 +++--- .../chat_single_contact_list_widget.dart | 129 +++++++++--------- 2 files changed, 78 insertions(+), 89 deletions(-) diff --git a/lib/chat_list/views/chat_single_contact_item_widget.dart b/lib/chat_list/views/chat_single_contact_item_widget.dart index 3632003..746af91 100644 --- a/lib/chat_list/views/chat_single_contact_item_widget.dart +++ b/lib/chat_list/views/chat_single_contact_item_widget.dart @@ -1,9 +1,12 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_slidable/flutter_slidable.dart'; import 'package:flutter_translate/flutter_translate.dart'; +import '../../chat/cubits/active_chat_cubit.dart'; import '../../proto/proto.dart' as proto; import '../../theme/theme.dart'; +import '../chat_list.dart'; class ChatSingleContactItemWidget extends StatelessWidget { const ChatSingleContactItemWidget({required proto.Contact contact, super.key}) @@ -20,10 +23,10 @@ class ChatSingleContactItemWidget extends StatelessWidget { //final textTheme = theme.textTheme; final scale = theme.extension()!; - final activeChat = ref.watch(activeChatStateProvider); + final activeChatCubit = context.watch(); final remoteConversationRecordKey = - proto.TypedKeyProto.fromProto(contact.remoteConversationRecordKey); - final selected = activeChat == remoteConversationRecordKey; + proto.TypedKeyProto.fromProto(_contact.remoteConversationRecordKey); + final selected = activeChatCubit.state == remoteConversationRecordKey; return Container( margin: const EdgeInsets.fromLTRB(0, 4, 0, 0), @@ -34,21 +37,16 @@ class ChatSingleContactItemWidget extends StatelessWidget { borderRadius: BorderRadius.circular(8), )), child: Slidable( - key: ObjectKey(contact), + key: ObjectKey(_contact), endActionPane: ActionPane( motion: const DrawerMotion(), children: [ SlidableAction( onPressed: (context) async { - final activeAccountInfo = - await ref.read(fetchActiveAccountProvider.future); - if (activeAccountInfo != null) { - await deleteChat( - activeAccountInfo: activeAccountInfo, - remoteConversationRecordKey: - remoteConversationRecordKey); - ref.invalidate(fetchChatListProvider); - } + final chatListCubit = context.read(); + await chatListCubit.deleteChat( + remoteConversationRecordKey: + remoteConversationRecordKey); }, backgroundColor: scale.tertiaryScale.background, foregroundColor: scale.tertiaryScale.text, @@ -68,16 +66,14 @@ class ChatSingleContactItemWidget extends StatelessWidget { // The child of the Slidable is what the user sees when the // component is not dragged. child: ListTile( - onTap: () async { - ref.read(activeChatStateProvider.notifier).state = - remoteConversationRecordKey; - ref.invalidate(fetchChatListProvider); + onTap: () { + activeChatCubit.setActiveChat(remoteConversationRecordKey); }, - title: Text(contact.editedProfile.name), + title: Text(_contact.editedProfile.name), /// xxx show last message here - subtitle: (contact.editedProfile.pronouns.isNotEmpty) - ? Text(contact.editedProfile.pronouns) + subtitle: (_contact.editedProfile.pronouns.isNotEmpty) + ? Text(_contact.editedProfile.pronouns) : null, iconColor: scale.tertiaryScale.background, textColor: scale.tertiaryScale.text, @@ -89,6 +85,6 @@ class ChatSingleContactItemWidget extends StatelessWidget { @override void debugFillProperties(DiagnosticPropertiesBuilder properties) { super.debugFillProperties(properties); - properties.add(DiagnosticsProperty('contact', contact)); + properties.add(DiagnosticsProperty('contact', _contact)); } } diff --git a/lib/chat_list/views/chat_single_contact_list_widget.dart b/lib/chat_list/views/chat_single_contact_list_widget.dart index 48db337..3d3f3eb 100644 --- a/lib/chat_list/views/chat_single_contact_list_widget.dart +++ b/lib/chat_list/views/chat_single_contact_list_widget.dart @@ -1,92 +1,85 @@ import 'package:awesome_extensions/awesome_extensions.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; -import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_translate/flutter_translate.dart'; import 'package:searchable_listview/searchable_listview.dart'; -import '../proto/proto.dart' as proto; -import '../tools/tools.dart'; +import '../../contacts/contacts.dart'; +import '../../proto/proto.dart' as proto; +import '../../theme/theme.dart'; +import '../../tools/tools.dart'; +import '../chat_list.dart'; import 'chat_single_contact_item_widget.dart'; import 'empty_chat_list_widget.dart'; -class ChatSingleContactListWidget extends ConsumerWidget { - ChatSingleContactListWidget( - {required IList contactList, - required this.chatList, - super.key}) - : contactMap = IMap.fromIterable(contactList, - keyMapper: (c) => c.remoteConversationRecordKey, - valueMapper: (c) => c); - - final IMap contactMap; - final IList chatList; +class ChatSingleContactListWidget extends StatelessWidget { + const ChatSingleContactListWidget({super.key}); @override // ignore: prefer_expression_function_bodies - Widget build(BuildContext context, WidgetRef ref) { + Widget build(BuildContext context) { final theme = Theme.of(context); //final textTheme = theme.textTheme; final scale = theme.extension()!; - return SizedBox.expand( - child: styledTitleContainer( - context: context, - title: translate('chat_list.chats'), - child: SizedBox.expand( - child: (chatList.isEmpty) - ? const EmptyChatListWidget() - : SearchableList( - autoFocusOnSearch: false, - initialList: chatList.toList(), - builder: (l, i, c) { - final contact = - contactMap[c.remoteConversationKey]; - if (contact == null) { - return const Text('...'); - } - return ChatSingleContactItemWidget( - contact: contact); - }, - filter: (value) { - final lowerValue = value.toLowerCase(); - return chatList.where((c) { + final contactListV = context.watch().state; + + return contactListV.builder((context, contactList) { + final contactMap = IMap.fromIterable(contactList, + keyMapper: (c) => c.remoteConversationRecordKey, + valueMapper: (c) => c); + + final chatListV = context.watch().state; + return chatListV.builder((context, chatList) => SizedBox.expand( + child: styledTitleContainer( + context: context, + title: translate('chat_list.chats'), + child: SizedBox.expand( + child: (chatList.isEmpty) + ? const EmptyChatListWidget() + : SearchableList( + autoFocusOnSearch: false, + initialList: chatList.toList(), + builder: (l, i, c) { final contact = contactMap[c.remoteConversationKey]; if (contact == null) { - return false; + return const Text('...'); } - return contact.editedProfile.name - .toLowerCase() - .contains(lowerValue) || - contact.editedProfile.pronouns - .toLowerCase() - .contains(lowerValue); - }).toList(); - }, - spaceBetweenSearchAndList: 4, - inputDecoration: InputDecoration( - labelText: translate('chat_list.search'), - contentPadding: const EdgeInsets.all(2), - fillColor: scale.primaryScale.text, - focusedBorder: OutlineInputBorder( - borderSide: BorderSide( - color: scale.primaryScale.hoverBorder, + return ChatSingleContactItemWidget( + contact: contact); + }, + filter: (value) { + final lowerValue = value.toLowerCase(); + return chatList.where((c) { + final contact = + contactMap[c.remoteConversationKey]; + if (contact == null) { + return false; + } + return contact.editedProfile.name + .toLowerCase() + .contains(lowerValue) || + contact.editedProfile.pronouns + .toLowerCase() + .contains(lowerValue); + }).toList(); + }, + spaceBetweenSearchAndList: 4, + inputDecoration: InputDecoration( + labelText: translate('chat_list.search'), + contentPadding: const EdgeInsets.all(2), + fillColor: scale.primaryScale.text, + focusedBorder: OutlineInputBorder( + borderSide: BorderSide( + color: scale.primaryScale.hoverBorder, + ), + borderRadius: BorderRadius.circular(8), ), - borderRadius: BorderRadius.circular(8), ), - ), - ).paddingAll(8)))) - .paddingLTRB(8, 8, 8, 65); - } - - @override - void debugFillProperties(DiagnosticPropertiesBuilder properties) { - super.debugFillProperties(properties); - properties - ..add(DiagnosticsProperty>( - 'contactMap', contactMap)) - ..add(IterableProperty('chatList', chatList)); + ).paddingAll(8)))) + .paddingLTRB(8, 8, 8, 65)); + }); } } From 2e4deb20381b40270f28981eedd13f946e2b092d Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Tue, 30 Jan 2024 19:47:22 -0500 Subject: [PATCH 23/68] everything but chat --- lib/chat/views/chat_component.dart | 34 ++----------------- lib/chat_list/views/views.dart | 3 ++ .../views/invite_dialog.dart | 7 ++-- lib/contacts/views/contact_item_widget.dart | 26 +++++--------- .../main_pager/chats_page.dart | 17 ++-------- lib/layout/layout.dart | 1 + 6 files changed, 23 insertions(+), 65 deletions(-) diff --git a/lib/chat/views/chat_component.dart b/lib/chat/views/chat_component.dart index 323b47c..270847d 100644 --- a/lib/chat/views/chat_component.dart +++ b/lib/chat/views/chat_component.dart @@ -1,46 +1,18 @@ import 'dart:async'; import 'package:awesome_extensions/awesome_extensions.dart'; -import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_chat_types/flutter_chat_types.dart' as types; import 'package:flutter_chat_ui/flutter_chat_ui.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import '../../old_to_refactor/proto/proto.dart' as proto; -import '../../old_to_refactor/providers/account.dart'; -import '../../old_to_refactor/providers/chat.dart'; -import '../../contacts/models/conversation.dart'; -import '../../old_to_refactor/tools/tools.dart'; -import '../../old_to_refactor/veilid_init.dart'; -import '../../old_to_refactor/veilid_support/veilid_support.dart'; - -class ChatComponent extends ConsumerStatefulWidget { - const ChatComponent( - {required this.activeAccountInfo, - required this.activeChat, - required this.activeChatContact, - super.key}); - - final ActiveAccountInfo activeAccountInfo; - final TypedKey activeChat; - final proto.Contact activeChatContact; +class ChatComponent extends StatefulWidget { + const ChatComponent({super.key}); @override ChatComponentState createState() => ChatComponentState(); - @override - void debugFillProperties(DiagnosticPropertiesBuilder properties) { - super.debugFillProperties(properties); - properties - ..add(DiagnosticsProperty( - 'activeAccountInfo', activeAccountInfo)) - ..add(DiagnosticsProperty('activeChat', activeChat)) - ..add(DiagnosticsProperty( - 'activeChatContact', activeChatContact)); - } } -class ChatComponentState extends ConsumerState { +class ChatComponentState extends State { final _unfocusNode = FocusNode(); late final types.User _localUser; late final types.User _remoteUser; diff --git a/lib/chat_list/views/views.dart b/lib/chat_list/views/views.dart index e69de29..311d02e 100644 --- a/lib/chat_list/views/views.dart +++ b/lib/chat_list/views/views.dart @@ -0,0 +1,3 @@ +export 'chat_single_contact_item_widget.dart'; +export 'chat_single_contact_list_widget.dart'; +export 'empty_chat_list_widget.dart'; diff --git a/lib/contact_invitation/views/invite_dialog.dart b/lib/contact_invitation/views/invite_dialog.dart index ae24a68..f09a1e5 100644 --- a/lib/contact_invitation/views/invite_dialog.dart +++ b/lib/contact_invitation/views/invite_dialog.dart @@ -7,6 +7,7 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_translate/flutter_translate.dart'; import '../../account_manager/account_manager.dart'; +import '../../contacts/contacts.dart'; import '../../tools/tools.dart'; import '../contact_invitation.dart'; @@ -69,6 +70,7 @@ class InviteDialogState extends State { Future _onAccept() async { final navigator = Navigator.of(context); final activeAccountInfo = context.read(); + final contactList = context.read(); setState(() { _isAccepting = true; @@ -83,9 +85,8 @@ class InviteDialogState extends State { activeAccountInfo.localAccount.identityMaster.identityPublicKey == acceptedContact.remoteIdentity.identityPublicKey; if (!isSelf) { - await createContact( - activeAccountInfo: activeAccountInfo, - profile: acceptedContact.remoteProfile, + await contactList.createContact( + remoteProfile: acceptedContact.remoteProfile, remoteIdentity: acceptedContact.remoteIdentity, remoteConversationRecordKey: acceptedContact.remoteConversationRecordKey, diff --git a/lib/contacts/views/contact_item_widget.dart b/lib/contacts/views/contact_item_widget.dart index 0d31d80..52cfb42 100644 --- a/lib/contacts/views/contact_item_widget.dart +++ b/lib/contacts/views/contact_item_widget.dart @@ -5,6 +5,7 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_slidable/flutter_slidable.dart'; import 'package:flutter_translate/flutter_translate.dart'; import '../../chat_list/chat_list.dart'; +import '../../layout/layout.dart'; import '../../proto/proto.dart' as proto; import '../../theme/theme.dart'; import '../contacts.dart'; @@ -70,23 +71,14 @@ class ContactItemWidget extends StatelessWidget { // component is not dragged. child: ListTile( onTap: () async { - final activeAccountInfo = - await ref.read(fetchActiveAccountProvider.future); - if (activeAccountInfo != null) { - // Start a chat - await getOrCreateChatSingleContact( - activeAccountInfo: activeAccountInfo, - remoteConversationRecordKey: remoteConversationKey); - ref - ..invalidate(fetchContactListProvider) - ..invalidate(fetchChatListProvider); - // Click over to chats - if (context.mounted) { - await MainPager.of(context)?.pageController.animateToPage( - 1, - duration: 250.ms, - curve: Curves.easeInOut); - } + // Start a chat + final chatListCubit = context.read(); + await chatListCubit.getOrCreateChatSingleContact( + remoteConversationRecordKey: remoteConversationKey); + // Click over to chats + if (context.mounted) { + await MainPager.of(context)?.pageController.animateToPage(1, + duration: 250.ms, curve: Curves.easeInOut); } // // ignore: use_build_context_synchronously diff --git a/lib/layout/home/home_account_ready/main_pager/chats_page.dart b/lib/layout/home/home_account_ready/main_pager/chats_page.dart index 4b663fd..1c7e7fe 100644 --- a/lib/layout/home/home_account_ready/main_pager/chats_page.dart +++ b/lib/layout/home/home_account_ready/main_pager/chats_page.dart @@ -1,10 +1,7 @@ import 'package:awesome_extensions/awesome_extensions.dart'; -import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:flutter/material.dart'; -import '../../../../proto/proto.dart' as proto; -import '../../../account_manager/account_manager.dart'; -import '../../../tools/tools.dart'; +import '../../../../chat_list/chat_list.dart'; class ChatsPage extends StatefulWidget { const ChatsPage({super.key}); @@ -30,16 +27,8 @@ class ChatsPageState extends State { @override // ignore: prefer_expression_function_bodies Widget build(BuildContext context) { - final contactList = ref.watch(fetchContactListProvider).asData?.value ?? - const IListConst([]); - final chatList = - ref.watch(fetchChatListProvider).asData?.value ?? const IListConst([]); - return Column(children: [ - if (chatList.isNotEmpty) - ChatSingleContactListWidget( - contactList: contactList, chatList: chatList) - .expanded(), - if (chatList.isEmpty) const EmptyChatListWidget().expanded(), + const ChatSingleContactListWidget().expanded(), ]); + } } diff --git a/lib/layout/layout.dart b/lib/layout/layout.dart index 90c50e7..003d97a 100644 --- a/lib/layout/layout.dart +++ b/lib/layout/layout.dart @@ -1,3 +1,4 @@ export 'default_app_bar.dart'; export 'home/home.dart'; +export 'home/home_account_ready/main_pager/main_pager.dart'; export 'index.dart'; From cd5d10ec1ffca867fc2b5248462cbf8190e6b756 Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Wed, 31 Jan 2024 22:29:06 -0500 Subject: [PATCH 24/68] conversation cubit work --- .../cubit/account_record_cubit.dart | 2 +- lib/chat/cubits/messages_cubit.dart | 0 lib/chat/views/chat_component.dart | 7 +- lib/contacts/cubits/conversation_cubit.dart | 295 ++++++++++++++++++ lib/tools/stack_trace.dart | 2 +- .../lib/dht_support/src/dht_record_cubit.dart | 105 +++++-- pubspec.lock | 196 ++++++------ pubspec.yaml | 76 ++--- 8 files changed, 515 insertions(+), 168 deletions(-) create mode 100644 lib/chat/cubits/messages_cubit.dart create mode 100644 lib/contacts/cubits/conversation_cubit.dart diff --git a/lib/account_manager/cubit/account_record_cubit.dart b/lib/account_manager/cubit/account_record_cubit.dart index 65306dd..b62d37a 100644 --- a/lib/account_manager/cubit/account_record_cubit.dart +++ b/lib/account_manager/cubit/account_record_cubit.dart @@ -7,7 +7,7 @@ import '../../proto/proto.dart' as proto; class AccountRecordCubit extends DefaultDHTRecordCubit { AccountRecordCubit({ required super.record, - }) : super(decodeState: proto.Account.fromBuffer); + }) : super.value(decodeState: proto.Account.fromBuffer); @override Future close() async { diff --git a/lib/chat/cubits/messages_cubit.dart b/lib/chat/cubits/messages_cubit.dart new file mode 100644 index 0000000..e69de29 diff --git a/lib/chat/views/chat_component.dart b/lib/chat/views/chat_component.dart index 270847d..dcfc8a1 100644 --- a/lib/chat/views/chat_component.dart +++ b/lib/chat/views/chat_component.dart @@ -2,9 +2,13 @@ import 'dart:async'; import 'package:awesome_extensions/awesome_extensions.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_chat_types/flutter_chat_types.dart' as types; import 'package:flutter_chat_ui/flutter_chat_ui.dart'; +import '../../theme/theme.dart'; +import '../chat.dart'; + class ChatComponent extends StatefulWidget { const ChatComponent({super.key}); @@ -144,8 +148,7 @@ class ChatComponentState extends State { IconButton( icon: const Icon(Icons.close), onPressed: () async { - ref.read(activeChatStateProvider.notifier).state = - null; + context.read().setActiveChat(null); }).paddingLTRB(16, 0, 16, 0) ]), ), diff --git a/lib/contacts/cubits/conversation_cubit.dart b/lib/contacts/cubits/conversation_cubit.dart new file mode 100644 index 0000000..973cf95 --- /dev/null +++ b/lib/contacts/cubits/conversation_cubit.dart @@ -0,0 +1,295 @@ +// A Conversation is a type of Chat that is 1:1 between two Contacts only +// Each Contact in the ContactList has at most one Conversation between the +// remote contact and the local account + +import 'dart:async'; +import 'dart:convert'; + +import 'package:equatable/equatable.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:meta/meta.dart'; +import 'package:veilid_support/veilid_support.dart'; + +import '../../account_manager/account_manager.dart'; +import '../../proto/proto.dart' as proto; + +@immutable +class ConversationState extends Equatable { + const ConversationState( + {required this.localConversation, required this.remoteConversation}); + + final proto.Conversation? localConversation; + final proto.Conversation? remoteConversation; + + @override + List get props => [localConversation, remoteConversation]; +} + +class ConversationCubit extends Cubit> { + ConversationCubit( + {required ActiveAccountInfo activeAccountInfo, + required TypedKey remoteIdentityPublicKey, + TypedKey? localConversationRecordKey, + TypedKey? remoteConversationRecordKey}) + : _activeAccountInfo = activeAccountInfo, + _localConversationRecordKey = localConversationRecordKey, + _remoteIdentityPublicKey = remoteIdentityPublicKey, + _remoteConversationRecordKey = remoteConversationRecordKey, + _incrementalState = const ConversationState( + localConversation: null, remoteConversation: null), + super(const AsyncValue.loading()) { + if (_localConversationRecordKey != null) { + Future.delayed(Duration.zero, () async { + final accountRecordKey = _activeAccountInfo + .userLogin.accountRecordInfo.accountRecord.recordKey; + + // Open local record key if it is specified + final pool = DHTRecordPool.instance; + final crypto = await getConversationCrypto(); + final writer = _activeAccountInfo.conversationWriter; + final record = await pool.openWrite( + _localConversationRecordKey!, writer, + parent: accountRecordKey, crypto: crypto); + await _setLocalConversation(record); + }); + } + + if (_remoteConversationRecordKey != null) { + Future.delayed(Duration.zero, () async { + final accountRecordKey = _activeAccountInfo + .userLogin.accountRecordInfo.accountRecord.recordKey; + + // Open remote record key if it is specified + final pool = DHTRecordPool.instance; + final crypto = await getConversationCrypto(); + final record = await pool.openRead(_remoteConversationRecordKey!, + parent: accountRecordKey, crypto: crypto); + await _setRemoteConversation(record); + }); + } + } + + @override + Future close() async { + await super.close(); + } + + // Open local converation key + Future _setLocalConversation(DHTRecord localConversationRecord) async { + _localConversationCubit = DefaultDHTRecordCubit.value( + record: localConversationRecord, + decodeState: proto.Conversation.fromBuffer); + _localConversationCubit!.stream.listen((avconv) { + final newState = avconv.when( + data: (conv) { + _incrementalState = ConversationState( + localConversation: conv, + remoteConversation: _incrementalState.remoteConversation); + // return loading still if state isn't complete + if ((_localConversationRecordKey != null && + _incrementalState.localConversation == null) || + (_remoteConversationRecordKey != null && + _incrementalState.remoteConversation == null)) { + return const AsyncValue.loading(); + } + // state is complete, all required keys are open + return AsyncValue.data(_incrementalState); + }, + loading: AsyncValue.loading, + error: AsyncValue.error, + ); + emit(newState); + }); + } + + // Open remote converation key + Future _setRemoteConversation( + DHTRecord remoteConversationRecord) async { + _remoteConversationCubit = DefaultDHTRecordCubit.value( + record: remoteConversationRecord, + decodeState: proto.Conversation.fromBuffer); + _remoteConversationCubit!.stream.listen((avconv) { + final newState = avconv.when( + data: (conv) { + _incrementalState = ConversationState( + localConversation: _incrementalState.localConversation, + remoteConversation: conv); + // return loading still if state isn't complete + if ((_localConversationRecordKey != null && + _incrementalState.localConversation == null) || + (_remoteConversationRecordKey != null && + _incrementalState.remoteConversation == null)) { + return const AsyncValue.loading(); + } + // state is complete, all required keys are open + return AsyncValue.data(_incrementalState); + }, + loading: AsyncValue.loading, + error: AsyncValue.error, + ); + emit(newState); + }); + } + + // Initialize a local conversation + // If we were the initiator of the conversation there may be an + // incomplete 'existingConversationRecord' that we need to fill + // in now that we have the remote identity key + // The ConversationCubit must not already have a local conversation + Future initLocalConversation( + {required proto.Profile profile, + required FutureOr Function(DHTRecord) callback, + TypedKey? existingConversationRecordKey}) async { + assert(_localConversationRecordKey == null, + 'must not have a local conversation yet'); + + final pool = DHTRecordPool.instance; + final accountRecordKey = + _activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey; + + final crypto = await getConversationCrypto(); + final writer = _activeAccountInfo.conversationWriter; + + // Open with SMPL scheme for identity writer + late final DHTRecord localConversationRecord; + if (existingConversationRecordKey != null) { + localConversationRecord = await pool.openWrite( + existingConversationRecordKey, writer, + parent: accountRecordKey, crypto: crypto); + } else { + final localConversationRecordCreate = await pool.create( + parent: accountRecordKey, + crypto: crypto, + schema: DHTSchema.smpl( + oCnt: 0, members: [DHTSchemaMember(mKey: writer.key, mCnt: 1)])); + await localConversationRecordCreate.close(); + localConversationRecord = await pool.openWrite( + localConversationRecordCreate.key, writer, + parent: accountRecordKey, crypto: crypto); + } + final out = localConversationRecord + // ignore: prefer_expression_function_bodies + .deleteScope((localConversation) async { + // Make messages log + return (await DHTShortArray.create( + parent: localConversation.key, + crypto: crypto, + smplWriter: writer)) + .deleteScope((messages) async { + // Write local conversation key + final conversation = proto.Conversation() + ..profile = profile + ..identityMasterJson = jsonEncode( + _activeAccountInfo.localAccount.identityMaster.toJson()) + ..messages = messages.record.key.toProto(); + + // + final update = await localConversation.tryWriteProtobuf( + proto.Conversation.fromBuffer, conversation); + if (update != null) { + throw Exception('Failed to write local conversation'); + } + return await callback(localConversation); + }); + }); + + // If success, save the new local conversation record key in this object + _localConversationRecordKey = localConversationRecord.key; + await _setLocalConversation(localConversationRecord); + + return out; + } + + // Force refresh of conversation keys + Future refresh() async { + if (_localConversationCubit != null) { + xxx use defaultdhtrecordcubit refresh mechanism + } + } + + // Future readRemoteConversation() async { + // final accountRecordKey = + // _activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey; + // final pool = DHTRecordPool.instance; + + // final crypto = await getConversationCrypto(); + // return (await pool.openRead(_remoteConversationRecordKey!, + // parent: accountRecordKey, crypto: crypto)) + // .scope((remoteConversation) async { + // // + // final conversation = + // await remoteConversation.getProtobuf(proto.Conversation.fromBuffer); + // return conversation; + // }); + // } + + // Future readLocalConversation() async { + // final accountRecordKey = + // _activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey; + // final pool = DHTRecordPool.instance; + + // final crypto = await getConversationCrypto(); + // return (await pool.openRead(_localConversationRecordKey!, + // parent: accountRecordKey, crypto: crypto)) + // .scope((localConversation) async { + // // + // final update = + // await localConversation.getProtobuf(proto.Conversation.fromBuffer); + // if (update != null) { + // return update; + // } + // return null; + // }); + // } + + // Future writeLocalConversation({ + // required proto.Conversation conversation, + // }) async { + // final accountRecordKey = + // _activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey; + // final pool = DHTRecordPool.instance; + + // final crypto = await getConversationCrypto(); + // final writer = _activeAccountInfo.conversationWriter; + + // return (await pool.openWrite(_localConversationRecordKey!, writer, + // parent: accountRecordKey, crypto: crypto)) + // .scope((localConversation) async { + // // + // final update = await localConversation.tryWriteProtobuf( + // proto.Conversation.fromBuffer, conversation); + // if (update != null) { + // return update; + // } + // return null; + // }); + // } + + // + + Future getConversationCrypto() async { + var conversationCrypto = _conversationCrypto; + if (conversationCrypto != null) { + return conversationCrypto; + } + final identitySecret = _activeAccountInfo.userLogin.identitySecret; + final cs = await Veilid.instance.getCryptoSystem(identitySecret.kind); + final sharedSecret = + await cs.cachedDH(_remoteIdentityPublicKey.value, identitySecret.value); + + conversationCrypto = await DHTRecordCryptoPrivate.fromSecret( + identitySecret.kind, sharedSecret); + _conversationCrypto = conversationCrypto; + return conversationCrypto; + } + + final ActiveAccountInfo _activeAccountInfo; + final TypedKey _remoteIdentityPublicKey; + TypedKey? _localConversationRecordKey; + TypedKey? _remoteConversationRecordKey; + DefaultDHTRecordCubit? _localConversationCubit; + DefaultDHTRecordCubit? _remoteConversationCubit; + ConversationState _incrementalState; + // + DHTRecordCrypto? _conversationCrypto; +} diff --git a/lib/tools/stack_trace.dart b/lib/tools/stack_trace.dart index ec21f24..6cd7f55 100644 --- a/lib/tools/stack_trace.dart +++ b/lib/tools/stack_trace.dart @@ -6,7 +6,7 @@ Never throwErrorWithCombinedStackTrace(Object error, StackTrace stackTrace) { final chain = Chain([ Trace.current(), ...Chain.forTrace(stackTrace).traces, - ]); // .foldFrames((frame) => frame.package == 'riverpod'); + ]); // .foldFrames((frame) => frame.package == 'xxx'); Error.throwWithStackTrace(error, chain.toTrace().vmTrace); } diff --git a/packages/veilid_support/lib/dht_support/src/dht_record_cubit.dart b/packages/veilid_support/lib/dht_support/src/dht_record_cubit.dart index 00db3fd..78e6eb6 100644 --- a/packages/veilid_support/lib/dht_support/src/dht_record_cubit.dart +++ b/packages/veilid_support/lib/dht_support/src/dht_record_cubit.dart @@ -7,33 +7,58 @@ import '../../veilid_support.dart'; class DHTRecordCubit extends Cubit> { DHTRecordCubit({ + required Future Function() open, + required Future Function(DHTRecord) initialStateFunction, + required Future Function(DHTRecord, List, ValueData) + stateFunction, + }) : _wantsCloseRecord = false, + super(const AsyncValue.loading()) { + Future.delayed(Duration.zero, () async { + // Do record open/create + _record = await open(); + _wantsCloseRecord = true; + await _init(initialStateFunction, stateFunction); + }); + } + + DHTRecordCubit.value({ required DHTRecord record, required Future Function(DHTRecord) initialStateFunction, required Future Function(DHTRecord, List, ValueData) stateFunction, - }) : super(const AsyncValue.loading()) { + }) : _record = record, + _wantsCloseRecord = false, + super(const AsyncValue.loading()) { Future.delayed(Duration.zero, () async { - // Make initial state update + await _init(initialStateFunction, stateFunction); + }); + } + + Future _init( + Future Function(DHTRecord) initialStateFunction, + Future Function(DHTRecord, List, ValueData) + stateFunction, + ) async { + // Make initial state update + try { + final initialState = await initialStateFunction(_record); + if (initialState != null) { + emit(AsyncValue.data(initialState)); + } + } on Exception catch (e) { + emit(AsyncValue.error(e)); + } + + _subscription = await _record.listen((update) async { try { - final initialState = await initialStateFunction(record); - if (initialState != null) { - emit(AsyncValue.data(initialState)); + final newState = + await stateFunction(_record, update.subkeys, update.valueData); + if (newState != null) { + emit(AsyncValue.data(newState)); } } on Exception catch (e) { emit(AsyncValue.error(e)); } - - _subscription = await record.listen((update) async { - try { - final newState = - await stateFunction(record, update.subkeys, update.valueData); - if (newState != null) { - emit(AsyncValue.data(newState)); - } - } on Exception catch (e) { - emit(AsyncValue.error(e)); - } - }); }); } @@ -41,26 +66,48 @@ class DHTRecordCubit extends Cubit> { Future close() async { await _subscription?.cancel(); _subscription = null; + if (_wantsCloseRecord) { + await _record.close(); + _wantsCloseRecord = false; + } await super.close(); } StreamSubscription? _subscription; + late DHTRecord _record; + bool _wantsCloseRecord; } // Cubit that watches the default subkey value of a dhtrecord class DefaultDHTRecordCubit extends DHTRecordCubit { DefaultDHTRecordCubit({ - required super.record, + required super.open, required T Function(List data) decodeState, }) : super( - initialStateFunction: (record) async { - final initialData = await record.get(); - if (initialData == null) { - return null; - } - return decodeState(initialData); - }, - stateFunction: (record, subkeys, valueData) async { + initialStateFunction: _makeInitialStateFunction(decodeState), + stateFunction: _makeStateFunction(decodeState)); + + DefaultDHTRecordCubit.value({ + required super.record, + required T Function(List data) decodeState, + }) : super.value( + initialStateFunction: _makeInitialStateFunction(decodeState), + stateFunction: _makeStateFunction(decodeState), + ); + + static Future Function(DHTRecord) _makeInitialStateFunction( + T Function(List data) decodeState) => + (record) async { + final initialData = await record.get(); + if (initialData == null) { + return null; + } + return decodeState(initialData); + }; + + static Future Function(DHTRecord, List, ValueData) + _makeStateFunction(T Function(List data) decodeState) => + (record, subkeys, valueData) async { final defaultSubkey = record.subkeyOrDefault(-1); if (subkeys.containsSubkey(defaultSubkey)) { final Uint8List data; @@ -78,6 +125,8 @@ class DefaultDHTRecordCubit extends DHTRecordCubit { return newState; } return null; - }, - ); + }; + + xxx add refresh/get mechanism to DHTRecordCubit and here too, then propagage to conversation_cubit + xxx should just be a 'get' like in dht_short_array_cubit } diff --git a/pubspec.lock b/pubspec.lock index ee6c0fa..648e896 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -37,10 +37,10 @@ packages: dependency: "direct main" description: name: archive - sha256: "7b875fd4a20b165a3084bd2d210439b22ebc653f21cea4842729c0c30c82596b" + sha256: "22600aa1e926be775fa5fe7e6894e7fb3df9efda8891c73f70fb3262399a432d" url: "https://pub.dev" source: hosted - version: "3.4.9" + version: "3.4.10" args: dependency: transitive description: @@ -61,10 +61,10 @@ packages: dependency: "direct main" description: name: awesome_extensions - sha256: a8b68d567119b9be85bc62d8dc2ab6712d74c0130bdc31a52c53d1058c4fe33a + sha256: cde9c8c155c1a1cafc5286807e16124e97f0cff739a47ec17aa9d26c3c37abcf url: "https://pub.dev" source: hosted - version: "2.0.11" + version: "2.0.12" badges: dependency: "direct main" description: @@ -141,18 +141,18 @@ packages: dependency: "direct dev" description: name: build_runner - sha256: "67d591d602906ef9201caf93452495ad1812bea2074f04e25dbd7c133785821b" + sha256: "581bacf68f89ec8792f5e5a0b2c4decd1c948e97ce659dc783688c8a88fbec21" url: "https://pub.dev" source: hosted - version: "2.4.7" + version: "2.4.8" build_runner_core: dependency: transitive description: name: build_runner_core - sha256: c9e32d21dd6626b5c163d48b037ce906bbe428bc23ab77bcd77bb21e593b6185 + sha256: "4ae8ffe5ac758da294ecf1802f2aff01558d8b1b00616aa7538ea9a8a5d50799" url: "https://pub.dev" source: hosted - version: "7.2.11" + version: "7.3.0" built_collection: dependency: transitive description: @@ -165,74 +165,74 @@ packages: dependency: transitive description: name: built_value - sha256: c9aabae0718ec394e5bc3c7272e6bb0dc0b32201a08fe185ec1d8401d3e39309 + sha256: a3ec2e0f967bc47f69f95009bb93db936288d61d5343b9436e378b28a2f830c6 url: "https://pub.dev" source: hosted - version: "8.8.1" + version: "8.9.0" cached_network_image: dependency: transitive description: name: cached_network_image - sha256: f98972704692ba679db144261172a8e20feb145636c617af0eb4022132a6797f + sha256: "28ea9690a8207179c319965c13cd8df184d5ee721ae2ce60f398ced1219cea1f" url: "https://pub.dev" source: hosted - version: "3.3.0" + version: "3.3.1" cached_network_image_platform_interface: dependency: transitive description: name: cached_network_image_platform_interface - sha256: "56aa42a7a01e3c9db8456d9f3f999931f1e05535b5a424271e9a38cabf066613" + sha256: "9e90e78ae72caa874a323d78fa6301b3fb8fa7ea76a8f96dc5b5bf79f283bf2f" url: "https://pub.dev" source: hosted - version: "3.0.0" + version: "4.0.0" cached_network_image_web: dependency: transitive description: name: cached_network_image_web - sha256: "759b9a9f8f6ccbb66c185df805fac107f05730b1dab9c64626d1008cca532257" + sha256: "42a835caa27c220d1294311ac409a43361088625a4f23c820b006dd9bffb3316" url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "1.1.1" camera: dependency: transitive description: name: camera - sha256: "7fa53bb1c2059e58bf86b7ab506e3b2a78e42f82d365b44b013239b975a166ef" + sha256: "9499cbc2e51d8eb0beadc158b288380037618ce4e30c9acbc4fae1ac3ecb5797" url: "https://pub.dev" source: hosted - version: "0.10.5+7" + version: "0.10.5+9" camera_android: dependency: transitive description: name: camera_android - sha256: "7215e38fa0be58cc3203a6e48de3636fb9b1bf93d6eeedf667f882d51b3a4bf3" + sha256: "351429510121d179b9aac5a2e8cb525c3cd6c39f4d709c5f72dfb21726e52371" url: "https://pub.dev" source: hosted - version: "0.10.8+15" + version: "0.10.8+16" camera_avfoundation: dependency: transitive description: name: camera_avfoundation - sha256: "3c8dd395f18722f01b5f325ddd7f5256e9bcdce538fb9243b378ba759df3283c" + sha256: "7d0763dfcbf060f56aa254a68c103210280bee9e97bbe4fdef23e257a4f70ab9" url: "https://pub.dev" source: hosted - version: "0.9.13+8" + version: "0.9.14" camera_platform_interface: dependency: transitive description: name: camera_platform_interface - sha256: b6a568984254cadaca41a6b896d87d3b2e79a2e5791afa036f8d524c6783b93a + sha256: e971ebca970f7cfee396f76ef02070b5e441b4aa04942da9c108d725f57bbd32 url: "https://pub.dev" source: hosted - version: "2.7.0" + version: "2.7.2" camera_web: dependency: transitive description: name: camera_web - sha256: d4c2c571c7af04f8b10702ca16bb9ed2a26e64534171e8f75c9349b2c004d8f1 + sha256: f18ccfb33b2a7c49a52ad5aa3f07330b7422faaecbdfd9b9fe8e51182f6ad67d url: "https://pub.dev" source: hosted - version: "0.3.2+3" + version: "0.3.2+4" change_case: dependency: "direct main" description: @@ -301,10 +301,10 @@ packages: dependency: transitive description: name: code_builder - sha256: feee43a5c05e7b3199bb375a86430b8ada1b04104f2923d0e03cc01ca87b6d84 + sha256: f692079e25e7869c14132d39f223f8eec9830eb76131925143b2129c4bb01b37 url: "https://pub.dev" source: hosted - version: "4.9.0" + version: "4.10.0" collection: dependency: transitive description: @@ -397,10 +397,10 @@ packages: dependency: "direct main" description: name: fast_immutable_collections - sha256: "3eb1d7495c70598964add20e10666003fad6e855b108fe684ebcbf8ad0c8e120" + sha256: b910ccdc99bb38a2abbce07c5afb8f81d4e222a892e4d095a548b99814837b0c url: "https://pub.dev" source: hosted - version: "9.2.0" + version: "9.2.1" ffi: dependency: transitive description: @@ -442,10 +442,10 @@ packages: dependency: "direct main" description: name: flutter_animate - sha256: "1dbc1aabfb8ec1e9d9feed2b675c21fb6b0a11f99be53ec3bc0f1901af6a8eb7" + sha256: cabe33af6201144be052352d53572a1b8a4f5782b46080be7520d95abe763715 url: "https://pub.dev" source: hosted - version: "4.3.0" + version: "4.4.1" flutter_bloc: dependency: "direct main" description: @@ -482,18 +482,18 @@ packages: dependency: "direct main" description: name: flutter_form_builder - sha256: "8973beed34b6d951d36bf688b52e9e3040b47b763c35c320bd6f4c2f6b13f3a2" + sha256: e8702c52dc45b43ed42e2b5d9b35f2970096d9cf1a58015cd3a76fad62a8f183 url: "https://pub.dev" source: hosted - version: "9.1.1" + version: "9.2.0" flutter_hooks: dependency: "direct main" description: name: flutter_hooks - sha256: "7c8db779c2d1010aa7f9ea3fbefe8f86524fcb87b69e8b0af31e1a4b55422dec" + sha256: "09f64db63fee3b2ab8b9038a1346be7d8986977fae3fec601275bf32455ccfc0" url: "https://pub.dev" source: hosted - version: "0.20.3" + version: "0.20.4" flutter_link_previewer: dependency: transitive description: @@ -519,10 +519,10 @@ packages: dependency: "direct main" description: name: flutter_native_splash - sha256: "141b20f15a2c4fe6e33c49257ca1bc114fc5c500b04fcbc8d75016bb86af672f" + sha256: "558f10070f03ee71f850a78f7136ab239a67636a294a44a06b6b7345178edb1e" url: "https://pub.dev" source: hosted - version: "2.3.8" + version: "2.3.10" flutter_parsed_text: dependency: transitive description: @@ -665,10 +665,10 @@ packages: dependency: transitive description: name: http - sha256: d4872660c46d929f6b8a9ef4e7a7eff7e49bbf0c4ec3f385ee32df5119175139 + sha256: a2bbf9d017fcced29139daa8ed2bba4ece450ab222871df93ca9eec6f80c34ba url: "https://pub.dev" source: hosted - version: "1.1.2" + version: "1.2.0" http_multi_server: dependency: transitive description: @@ -697,18 +697,18 @@ packages: dependency: "direct dev" description: name: icons_launcher - sha256: "3ed4560181f238e69ca5d55589d6946ef31e6a321c934251a26ce1d9e9867305" + sha256: "9b514ffed6ed69b232fd2bf34c44878c8526be71fc74129a658f35c04c9d4a9d" url: "https://pub.dev" source: hosted - version: "2.1.6" + version: "2.1.7" image: dependency: "direct main" description: name: image - sha256: "028f61960d56f26414eb616b48b04eb37d700cbe477b7fb09bf1d7ce57fd9271" + sha256: "004a2e90ce080f8627b5a04aecb4cdfac87d2c3f3b520aa291260be5a32c033d" url: "https://pub.dev" source: hosted - version: "4.1.3" + version: "4.1.4" intl: dependency: "direct main" description: @@ -809,26 +809,26 @@ packages: dependency: transitive description: name: mime - sha256: e4ff8e8564c03f255408decd16e7899da1733852a9110a58fe6d1b817684a63e + sha256: "2e123074287cc9fd6c09de8336dae606d1ddb88d9ac47358826db698c176a1f2" url: "https://pub.dev" source: hosted - version: "1.0.4" + version: "1.0.5" mobile_scanner: dependency: "direct main" description: name: mobile_scanner - sha256: c3e5bba1cb626b6ab4fc46610f72a136803f6854267967e19f4a4a6a31ff9b74 + sha256: "1b60b8f9d4ce0cb0e7d7bc223c955d083a0737bee66fa1fcfe5de48225e0d5b3" url: "https://pub.dev" source: hosted - version: "3.5.5" + version: "3.5.7" motion_toast: dependency: "direct main" description: name: motion_toast - sha256: "1bdd11696de9151804644d3dadcbcfaa55749db0353aeca150389ecdeb2eaaac" + sha256: e423584213b459d021fbdfcd8e02b22e3480ff0b3cef05dc4cde040595ebf084 url: "https://pub.dev" source: hosted - version: "2.7.10" + version: "2.8.0" mutex: dependency: "direct main" description: @@ -889,10 +889,10 @@ packages: dependency: "direct main" description: name: path_provider - sha256: a1aa8aaa2542a6bc57e381f132af822420216c80d4781f7aa085ca3229208aaa + sha256: b27217933eeeba8ff24845c34003b003b2b22151de3c908d0e679e8fe1aa078b url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.1.2" path_provider_android: dependency: transitive description: @@ -905,10 +905,10 @@ packages: dependency: transitive description: name: path_provider_foundation - sha256: "19314d595120f82aca0ba62787d58dde2cc6b5df7d2f0daf72489e38d1b57f2d" + sha256: "5a7999be66e000916500be4f15a3633ebceb8302719b47b9cc49ce924125350f" url: "https://pub.dev" source: hosted - version: "2.3.1" + version: "2.3.2" path_provider_linux: dependency: transitive description: @@ -921,10 +921,10 @@ packages: dependency: transitive description: name: path_provider_platform_interface - sha256: "94b1e0dd80970c1ce43d5d4e050a9918fce4f4a775e6142424c30a29a363265c" + sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.1.2" path_provider_windows: dependency: transitive description: @@ -961,10 +961,10 @@ packages: dependency: transitive description: name: platform - sha256: "0a279f0707af40c890e80b1e9df8bb761694c074ba7e1d4ab1bc4b728e200b59" + sha256: "12220bb4b65720483f8fa9450b4332347737cf8213dd2840d8b2c823e47243ec" url: "https://pub.dev" source: hosted - version: "3.1.3" + version: "3.1.4" platform_info: dependency: transitive description: @@ -977,18 +977,18 @@ packages: dependency: transitive description: name: plugin_platform_interface - sha256: f4f88d4a900933e7267e2b353594774fc0d07fb072b47eedcd5b54e1ea3269f8 + sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" url: "https://pub.dev" source: hosted - version: "2.1.7" + version: "2.1.8" pointycastle: dependency: transitive description: name: pointycastle - sha256: "7c1e5f0d23c9016c5bbd8b1473d0d3fb3fc851b876046039509e18e0c7485f2c" + sha256: "43ac87de6e10afabc85c445745a7b799e04de84cebaa4fd7bf55a5e1e9604d29" url: "https://pub.dev" source: hosted - version: "3.7.3" + version: "3.7.4" pool: dependency: transitive description: @@ -1121,18 +1121,18 @@ packages: dependency: "direct main" description: name: searchable_listview - sha256: "492bb75e133d1be902d2c1e8aa362f21127260106557492993432a4f5489494a" + sha256: fad781a412d1fec251c7a66e4aca49e49ab8b7104bda733b476d4b5c81891bea url: "https://pub.dev" source: hosted - version: "2.9.1" + version: "2.10.1" share_plus: dependency: "direct main" description: name: share_plus - sha256: f74fc3f1cbd99f39760182e176802f693fa0ec9625c045561cfad54681ea93dd + sha256: "3ef39599b00059db0990ca2e30fca0a29d8b37aae924d60063f8e0184cf20900" url: "https://pub.dev" source: hosted - version: "7.2.1" + version: "7.2.2" share_plus_platform_interface: dependency: transitive description: @@ -1161,10 +1161,10 @@ packages: dependency: transitive description: name: shared_preferences_foundation - sha256: "7bf53a9f2d007329ee6f3df7268fd498f8373602f943c975598bbb34649b62a7" + sha256: "7708d83064f38060c7b39db12aefe449cb8cdc031d6062280087bc4cdb988f5c" url: "https://pub.dev" source: hosted - version: "2.3.4" + version: "2.3.5" shared_preferences_linux: dependency: transitive description: @@ -1177,10 +1177,10 @@ packages: dependency: transitive description: name: shared_preferences_platform_interface - sha256: d4ec5fc9ebb2f2e056c617112aa75dcf92fc2e4faaf2ae999caa297473f75d8a + sha256: "22e2ecac9419b4246d7c22bfbbda589e3acf5c0351137d87dd2939d984d37c3b" url: "https://pub.dev" source: hosted - version: "2.3.1" + version: "2.3.2" shared_preferences_web: dependency: transitive description: @@ -1270,18 +1270,18 @@ packages: dependency: transitive description: name: sqflite - sha256: "591f1602816e9c31377d5f008c2d9ef7b8aca8941c3f89cc5fd9d84da0c38a9a" + sha256: a9016f495c927cb90557c909ff26a6d92d9bd54fc42ba92e19d4e79d61e798c6 url: "https://pub.dev" source: hosted - version: "2.3.0" + version: "2.3.2" sqflite_common: dependency: transitive description: name: sqflite_common - sha256: bb4738f15b23352822f4c42a531677e5c6f522e079461fd240ead29d8d8a54a6 + sha256: "28d8c66baee4968519fb8bd6cdbedad982d6e53359091f0b74544a9f32ec72d5" url: "https://pub.dev" source: hosted - version: "2.5.0+2" + version: "2.5.3" stack_trace: dependency: "direct main" description: @@ -1398,26 +1398,26 @@ packages: dependency: transitive description: name: url_launcher - sha256: e9aa5ea75c84cf46b3db4eea212523591211c3cf2e13099ee4ec147f54201c86 + sha256: c512655380d241a337521703af62d2c122bf7b77a46ff7dd750092aa9433499c url: "https://pub.dev" source: hosted - version: "6.2.2" + version: "6.2.4" url_launcher_android: dependency: transitive description: name: url_launcher_android - sha256: "31222ffb0063171b526d3e569079cf1f8b294075ba323443fdc690842bfd4def" + sha256: "507dc655b1d9cb5ebc756032eb785f114e415f91557b73bf60b7e201dfedeb2f" url: "https://pub.dev" source: hosted - version: "6.2.0" + version: "6.2.2" url_launcher_ios: dependency: transitive description: name: url_launcher_ios - sha256: bba3373219b7abb6b5e0d071b0fe66dfbe005d07517a68e38d4fc3638f35c6d3 + sha256: "75bb6fe3f60070407704282a2d295630cab232991eb52542b18347a8a941df03" url: "https://pub.dev" source: hosted - version: "6.2.1" + version: "6.2.4" url_launcher_linux: dependency: transitive description: @@ -1438,18 +1438,18 @@ packages: dependency: transitive description: name: url_launcher_platform_interface - sha256: "980e8d9af422f477be6948bdfb68df8433be71f5743a188968b0c1b887807e50" + sha256: a932c3a8082e118f80a475ce692fde89dc20fddb24c57360b96bc56f7035de1f url: "https://pub.dev" source: hosted - version: "2.2.0" + version: "2.3.1" url_launcher_web: dependency: transitive description: name: url_launcher_web - sha256: "7286aec002c8feecc338cc33269e96b73955ab227456e9fb2a91f7fab8a358e9" + sha256: fff0932192afeedf63cdd50ecbb1bc825d31aed259f02bb8dba0f3b729a5e88b url: "https://pub.dev" source: hosted - version: "2.2.2" + version: "2.2.3" url_launcher_windows: dependency: transitive description: @@ -1470,26 +1470,26 @@ packages: dependency: transitive description: name: vector_graphics - sha256: "0f0c746dd2d6254a0057218ff980fc7f5670fd0fcf5e4db38a490d31eed4ad43" + sha256: "18f6690295af52d081f6808f2f7c69f0eed6d7e23a71539d75f4aeb8f0062172" url: "https://pub.dev" source: hosted - version: "1.1.9+1" + version: "1.1.9+2" vector_graphics_codec: dependency: transitive description: name: vector_graphics_codec - sha256: "0edf6d630d1bfd5589114138ed8fada3234deacc37966bec033d3047c29248b7" + sha256: "531d20465c10dfac7f5cd90b60bbe4dd9921f1ec4ca54c83ebb176dbacb7bb2d" url: "https://pub.dev" source: hosted - version: "1.1.9+1" + version: "1.1.9+2" vector_graphics_compiler: dependency: transitive description: name: vector_graphics_compiler - sha256: d24333727332d9bd20990f1483af4e09abdb9b1fc7c3db940b56ab5c42790c26 + sha256: "03012b0a33775c5530576b70240308080e1d5050f0faf000118c20e6463bc0ad" url: "https://pub.dev" source: hosted - version: "1.1.9+1" + version: "1.1.9+2" vector_math: dependency: transitive description: @@ -1548,26 +1548,26 @@ packages: dependency: transitive description: name: win32 - sha256: b0f37db61ba2f2e9b7a78a1caece0052564d1bc70668156cf3a29d676fe4e574 + sha256: "464f5674532865248444b4c3daca12bd9bf2d7c47f759ce2617986e7229494a8" url: "https://pub.dev" source: hosted - version: "5.1.1" + version: "5.2.0" window_manager: dependency: "direct main" description: name: window_manager - sha256: dcc865277f26a7dad263a47d0e405d77e21f12cb71f30333a52710a408690bd7 + sha256: b3c895bdf936c77b83c5254bec2e6b3f066710c1f89c38b20b8acc382b525494 url: "https://pub.dev" source: hosted - version: "0.3.7" + version: "0.3.8" xdg_directories: dependency: transitive description: name: xdg_directories - sha256: "589ada45ba9e39405c198fe34eb0f607cddb2108527e658136120892beac46d2" + sha256: faea9dee56b520b55a566385b84f2e8de55e7496104adada9962e0bd11bcff1d url: "https://pub.dev" source: hosted - version: "1.0.3" + version: "1.0.4" xml: dependency: transitive description: @@ -1609,5 +1609,5 @@ packages: source: hosted version: "0.9.0" sdks: - dart: ">=3.2.0 <4.0.0" - flutter: ">=3.16.0" + dart: ">=3.2.3 <4.0.0" + flutter: ">=3.16.6" diff --git a/pubspec.yaml b/pubspec.yaml index f91527d..a5f22bc 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -8,65 +8,65 @@ environment: flutter: ">=3.10.0" dependencies: - animated_theme_switcher: ^2.0.7 - ansicolor: ^2.0.1 - archive: ^3.3.7 - awesome_extensions: ^2.0.9 - badges: ^3.1.1 - basic_utils: ^5.6.1 + animated_theme_switcher: ^2.0.10 + ansicolor: ^2.0.2 + archive: ^3.4.10 + awesome_extensions: ^2.0.12 + badges: ^3.1.2 + basic_utils: ^5.7.0 bloc: ^8.1.2 - blurry_modal_progress_hud: ^1.1.0 + blurry_modal_progress_hud: ^1.1.1 change_case: ^1.1.0 charcode: ^1.3.1 circular_profile_avatar: ^2.0.5 circular_reveal_animation: ^2.0.1 cool_dropdown: ^2.1.0 - cupertino_icons: ^1.0.2 + cupertino_icons: ^1.0.6 equatable: ^2.0.5 - fast_immutable_collections: ^9.1.5 + fast_immutable_collections: ^9.2.1 fixnum: ^1.1.0 flutter: sdk: flutter - flutter_animate: ^4.2.0+1 + flutter_animate: ^4.4.1 flutter_bloc: ^8.1.3 flutter_chat_types: ^3.6.2 - flutter_chat_ui: ^1.6.9 - flutter_form_builder: ^9.1.0 - flutter_hooks: ^0.20.1 + flutter_chat_ui: ^1.6.10 + flutter_form_builder: ^9.2.0 + flutter_hooks: ^0.20.4 flutter_localizations: sdk: flutter - flutter_native_splash: ^2.3.2 - flutter_slidable: ^3.0.0 + flutter_native_splash: ^2.3.10 + flutter_slidable: ^3.0.1 flutter_spinkit: ^5.2.0 - flutter_svg: ^2.0.7 + flutter_svg: ^2.0.9 flutter_translate: ^4.0.4 - form_builder_validators: ^9.0.0 - freezed_annotation: ^2.2.0 - go_router: ^11.0.0 + form_builder_validators: ^9.1.0 + freezed_annotation: ^2.4.1 + go_router: ^11.1.4 hydrated_bloc: ^9.1.3 - image: ^4.1.3 - intl: ^0.18.0 + image: ^4.1.4 + intl: ^0.18.1 json_annotation: ^4.8.1 loggy: ^2.0.3 meta: ^1.10.0 - mobile_scanner: ^3.5.1 - motion_toast: ^2.7.8 - mutex: ^3.0.1 + mobile_scanner: ^3.5.7 + motion_toast: ^2.8.0 + mutex: ^3.1.0 pasteboard: ^0.2.0 - path: ^1.8.2 - path_provider: ^2.0.11 + path: ^1.8.3 + path_provider: ^2.1.2 pinput: ^3.0.1 preload_page_view: ^0.2.0 - protobuf: ^3.0.0 + protobuf: ^3.1.0 provider: ^6.1.1 - qr_code_dart_scan: ^0.7.2 + qr_code_dart_scan: ^0.7.3 qr_flutter: ^4.1.0 - quickalert: ^1.0.1 + quickalert: ^1.0.2 radix_colors: ^1.0.4 - reorderable_grid: ^1.0.7 - searchable_listview: ^2.7.0 - share_plus: ^7.0.2 - shared_preferences: ^2.0.15 + reorderable_grid: ^1.0.10 + searchable_listview: ^2.10.1 + share_plus: ^7.2.2 + shared_preferences: ^2.2.2 signal_strength_indicator: ^0.4.1 split_view: ^3.2.1 stack_trace: ^1.11.1 @@ -78,16 +78,16 @@ dependencies: path: ../veilid/veilid-flutter veilid_support: path: packages/veilid_support - window_manager: ^0.3.5 + window_manager: ^0.3.8 xterm: ^3.5.0 - zxing2: ^0.2.0 + zxing2: ^0.2.1 dev_dependencies: - build_runner: ^2.4.6 + build_runner: ^2.4.8 flutter_test: sdk: flutter - freezed: ^2.3.5 - icons_launcher: ^2.1.3 + freezed: ^2.4.6 + icons_launcher: ^2.1.7 json_serializable: ^6.7.1 lint_hard: ^4.0.0 From d3fb4b5b6a565c77f6f0db68b05eca6466d6844d Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Thu, 1 Feb 2024 09:41:58 -0500 Subject: [PATCH 25/68] xfer --- .../lib/dht_support/src/dht_record.dart | 11 +++++-- .../lib/dht_support/src/dht_record_cubit.dart | 31 ++++++++++++------- .../lib/dht_support/src/dht_short_array.dart | 11 +++---- 3 files changed, 33 insertions(+), 20 deletions(-) diff --git a/packages/veilid_support/lib/dht_support/src/dht_record.dart b/packages/veilid_support/lib/dht_support/src/dht_record.dart index b6f1b6d..a9064d0 100644 --- a/packages/veilid_support/lib/dht_support/src/dht_record.dart +++ b/packages/veilid_support/lib/dht_support/src/dht_record.dart @@ -282,7 +282,9 @@ class DHTRecord { } Future> listen( - Future Function(VeilidUpdateValueChange update) onUpdate, + Future Function( + DHTRecord record, Uint8List data, List subkeys) + onUpdate, ) async { // Set up watch requirements watchController ??= @@ -293,7 +295,12 @@ class DHTRecord { return watchController!.stream.listen( (update) { - Future.delayed(Duration.zero, () => onUpdate(update)); + Future.delayed(Duration.zero, () async { + final out = await _crypto.decrypt( + update.valueData.data, update.subkeys.first.low); + + await onUpdate(this, out, update.subkeys); + }); }, cancelOnError: true, onError: (e) async { diff --git a/packages/veilid_support/lib/dht_support/src/dht_record_cubit.dart b/packages/veilid_support/lib/dht_support/src/dht_record_cubit.dart index 78e6eb6..94bd861 100644 --- a/packages/veilid_support/lib/dht_support/src/dht_record_cubit.dart +++ b/packages/veilid_support/lib/dht_support/src/dht_record_cubit.dart @@ -9,7 +9,7 @@ class DHTRecordCubit extends Cubit> { DHTRecordCubit({ required Future Function() open, required Future Function(DHTRecord) initialStateFunction, - required Future Function(DHTRecord, List, ValueData) + required Future Function(DHTRecord, List, Uint8List) stateFunction, }) : _wantsCloseRecord = false, super(const AsyncValue.loading()) { @@ -24,7 +24,7 @@ class DHTRecordCubit extends Cubit> { DHTRecordCubit.value({ required DHTRecord record, required Future Function(DHTRecord) initialStateFunction, - required Future Function(DHTRecord, List, ValueData) + required Future Function(DHTRecord, List, Uint8List) stateFunction, }) : _record = record, _wantsCloseRecord = false, @@ -36,7 +36,7 @@ class DHTRecordCubit extends Cubit> { Future _init( Future Function(DHTRecord) initialStateFunction, - Future Function(DHTRecord, List, ValueData) + Future Function(DHTRecord, List, Uint8List) stateFunction, ) async { // Make initial state update @@ -49,10 +49,9 @@ class DHTRecordCubit extends Cubit> { emit(AsyncValue.error(e)); } - _subscription = await _record.listen((update) async { + _subscription = await _record.listen((record, data, subkeys) async { try { - final newState = - await stateFunction(_record, update.subkeys, update.valueData); + final newState = await stateFunction(record, subkeys, data); if (newState != null) { emit(AsyncValue.data(newState)); } @@ -73,6 +72,16 @@ class DHTRecordCubit extends Cubit> { await super.close(); } + Future refresh(List subkeys) async { + for (final skr in subkeys) { + for (var sk = skr.low; sk <= skr.high; sk++) { + final data = await _record.get(subkey: sk, forceRefresh: true); + final newState = await stateFunction(_record, subkeys, data); + xxx continue here + } + } + } + StreamSubscription? _subscription; late DHTRecord _record; bool _wantsCloseRecord; @@ -105,9 +114,9 @@ class DefaultDHTRecordCubit extends DHTRecordCubit { return decodeState(initialData); }; - static Future Function(DHTRecord, List, ValueData) + static Future Function(DHTRecord, List, Uint8List) _makeStateFunction(T Function(List data) decodeState) => - (record, subkeys, valueData) async { + (record, subkeys, updatedata) async { final defaultSubkey = record.subkeyOrDefault(-1); if (subkeys.containsSubkey(defaultSubkey)) { final Uint8List data; @@ -119,7 +128,7 @@ class DefaultDHTRecordCubit extends DHTRecordCubit { } data = maybeData; } else { - data = valueData.data; + data = updatedata; } final newState = decodeState(data); return newState; @@ -127,6 +136,6 @@ class DefaultDHTRecordCubit extends DHTRecordCubit { return null; }; - xxx add refresh/get mechanism to DHTRecordCubit and here too, then propagage to conversation_cubit - xxx should just be a 'get' like in dht_short_array_cubit + // xxx add refresh/get mechanism to DHTRecordCubit and here too, then propagage to conversation_cubit + // xxx should just be a 'get' like in dht_short_array_cubit } diff --git a/packages/veilid_support/lib/dht_support/src/dht_short_array.dart b/packages/veilid_support/lib/dht_support/src/dht_short_array.dart index 55f28e3..40d8b90 100644 --- a/packages/veilid_support/lib/dht_support/src/dht_short_array.dart +++ b/packages/veilid_support/lib/dht_support/src/dht_short_array.dart @@ -721,21 +721,18 @@ class DHTShortArray { } // Called when a head or linked record changes - Future _onUpdateRecord(VeilidUpdateValueChange update) async { - final record = _head.linkedRecords.firstWhere( - (element) => element.key == update.key, - orElse: () => _headRecord); - + Future _onUpdateRecord( + DHTRecord record, Uint8List data, List subkeys) async { // If head record subkey zero changes, then the layout // of the dhtshortarray has changed var updateHead = false; - if (record == _headRecord && update.subkeys.containsSubkey(0)) { + if (record == _headRecord && subkeys.containsSubkey(0)) { updateHead = true; } // If we have any other subkeys to update, do them first final unord = List>.empty(growable: true); - for (final skr in update.subkeys) { + for (final skr in subkeys) { for (var subkey = skr.low; subkey <= skr.high; subkey++) { // Skip head subkey if (subkey == 0) { From 4fe8a07d45126b05273e50eb515ae3c7ab46ab88 Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Thu, 1 Feb 2024 11:46:09 -0500 Subject: [PATCH 26/68] checkpoint --- .../lib/dht_support/src/dht_record_cubit.dart | 21 +++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/packages/veilid_support/lib/dht_support/src/dht_record_cubit.dart b/packages/veilid_support/lib/dht_support/src/dht_record_cubit.dart index 94bd861..e156ba0 100644 --- a/packages/veilid_support/lib/dht_support/src/dht_record_cubit.dart +++ b/packages/veilid_support/lib/dht_support/src/dht_record_cubit.dart @@ -5,13 +5,17 @@ import 'package:bloc/bloc.dart'; import '../../veilid_support.dart'; +typedef InitialStateFunction = Future Function(DHTRecord); +typedef StateFunction = Future Function( + DHTRecord, List, Uint8List); + class DHTRecordCubit extends Cubit> { DHTRecordCubit({ required Future Function() open, - required Future Function(DHTRecord) initialStateFunction, - required Future Function(DHTRecord, List, Uint8List) - stateFunction, + required InitialStateFunction initialStateFunction, + required StateFunction stateFunction, }) : _wantsCloseRecord = false, + _stateFunction = stateFunction, super(const AsyncValue.loading()) { Future.delayed(Duration.zero, () async { // Do record open/create @@ -27,6 +31,7 @@ class DHTRecordCubit extends Cubit> { required Future Function(DHTRecord, List, Uint8List) stateFunction, }) : _record = record, + _stateFunction = stateFunction, _wantsCloseRecord = false, super(const AsyncValue.loading()) { Future.delayed(Duration.zero, () async { @@ -75,9 +80,12 @@ class DHTRecordCubit extends Cubit> { Future refresh(List subkeys) async { for (final skr in subkeys) { for (var sk = skr.low; sk <= skr.high; sk++) { - final data = await _record.get(subkey: sk, forceRefresh: true); - final newState = await stateFunction(_record, subkeys, data); - xxx continue here + final data = await _record.get( + subkey: sk, forceRefresh: true, onlyUpdates: true); + if (data != null) { + final newState = await _stateFunction(_record, subkeys, data); + xxx remove sk from update + } } } } @@ -85,6 +93,7 @@ class DHTRecordCubit extends Cubit> { StreamSubscription? _subscription; late DHTRecord _record; bool _wantsCloseRecord; + final StateFunction _stateFunction; } // Cubit that watches the default subkey value of a dhtrecord From f2caa7a0b33e106255cbf8c4f5593236ba9f212e Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Wed, 7 Feb 2024 19:13:32 -0500 Subject: [PATCH 27/68] conversation cubit work --- lib/contacts/cubits/conversation_cubit.dart | 184 ++++++++---------- .../lib/dht_support/src/dht_record_cubit.dart | 63 +++--- 2 files changed, 117 insertions(+), 130 deletions(-) diff --git a/lib/contacts/cubits/conversation_cubit.dart b/lib/contacts/cubits/conversation_cubit.dart index 973cf95..a260ad6 100644 --- a/lib/contacts/cubits/conversation_cubit.dart +++ b/lib/contacts/cubits/conversation_cubit.dart @@ -74,61 +74,69 @@ class ConversationCubit extends Cubit> { await super.close(); } + void updateLocalConversationState(AsyncValue avconv) { + final newState = avconv.when( + data: (conv) { + _incrementalState = ConversationState( + localConversation: conv, + remoteConversation: _incrementalState.remoteConversation); + // return loading still if state isn't complete + if ((_localConversationRecordKey != null && + _incrementalState.localConversation == null) || + (_remoteConversationRecordKey != null && + _incrementalState.remoteConversation == null)) { + return const AsyncValue.loading(); + } + // state is complete, all required keys are open + return AsyncValue.data(_incrementalState); + }, + loading: AsyncValue.loading, + error: AsyncValue.error, + ); + emit(newState); + } + + void updateRemoteConversationState(AsyncValue avconv) { + final newState = avconv.when( + data: (conv) { + _incrementalState = ConversationState( + localConversation: _incrementalState.localConversation, + remoteConversation: conv); + // return loading still if state isn't complete + if ((_localConversationRecordKey != null && + _incrementalState.localConversation == null) || + (_remoteConversationRecordKey != null && + _incrementalState.remoteConversation == null)) { + return const AsyncValue.loading(); + } + // state is complete, all required keys are open + return AsyncValue.data(_incrementalState); + }, + loading: AsyncValue.loading, + error: AsyncValue.error, + ); + emit(newState); + } + // Open local converation key Future _setLocalConversation(DHTRecord localConversationRecord) async { + assert(_localConversationCubit == null, + 'shoud not set local conversation twice'); _localConversationCubit = DefaultDHTRecordCubit.value( record: localConversationRecord, decodeState: proto.Conversation.fromBuffer); - _localConversationCubit!.stream.listen((avconv) { - final newState = avconv.when( - data: (conv) { - _incrementalState = ConversationState( - localConversation: conv, - remoteConversation: _incrementalState.remoteConversation); - // return loading still if state isn't complete - if ((_localConversationRecordKey != null && - _incrementalState.localConversation == null) || - (_remoteConversationRecordKey != null && - _incrementalState.remoteConversation == null)) { - return const AsyncValue.loading(); - } - // state is complete, all required keys are open - return AsyncValue.data(_incrementalState); - }, - loading: AsyncValue.loading, - error: AsyncValue.error, - ); - emit(newState); - }); + _localConversationCubit!.stream.listen(updateLocalConversationState); } // Open remote converation key Future _setRemoteConversation( DHTRecord remoteConversationRecord) async { + assert(_remoteConversationCubit == null, + 'shoud not set remote conversation twice'); _remoteConversationCubit = DefaultDHTRecordCubit.value( record: remoteConversationRecord, decodeState: proto.Conversation.fromBuffer); - _remoteConversationCubit!.stream.listen((avconv) { - final newState = avconv.when( - data: (conv) { - _incrementalState = ConversationState( - localConversation: _incrementalState.localConversation, - remoteConversation: conv); - // return loading still if state isn't complete - if ((_localConversationRecordKey != null && - _incrementalState.localConversation == null) || - (_remoteConversationRecordKey != null && - _incrementalState.remoteConversation == null)) { - return const AsyncValue.loading(); - } - // state is complete, all required keys are open - return AsyncValue.data(_incrementalState); - }, - loading: AsyncValue.loading, - error: AsyncValue.error, - ); - emit(newState); - }); + _remoteConversationCubit!.stream.listen(updateRemoteConversationState); } // Initialize a local conversation @@ -136,6 +144,8 @@ class ConversationCubit extends Cubit> { // incomplete 'existingConversationRecord' that we need to fill // in now that we have the remote identity key // The ConversationCubit must not already have a local conversation + // The callback allows for more initialization to occur and for + // cleanup to delete records upon failure of the callback Future initLocalConversation( {required proto.Profile profile, required FutureOr Function(DHTRecord) callback, @@ -176,20 +186,25 @@ class ConversationCubit extends Cubit> { crypto: crypto, smplWriter: writer)) .deleteScope((messages) async { - // Write local conversation key + // Create initial local conversation key contents final conversation = proto.Conversation() ..profile = profile ..identityMasterJson = jsonEncode( _activeAccountInfo.localAccount.identityMaster.toJson()) ..messages = messages.record.key.toProto(); - // + // Write initial conversation to record final update = await localConversation.tryWriteProtobuf( proto.Conversation.fromBuffer, conversation); if (update != null) { throw Exception('Failed to write local conversation'); } - return await callback(localConversation); + final out = await callback(localConversation); + + // Upon success emit the local conversation record to the state + updateLocalConversationState(AsyncValue.data(conversation)); + + return out; }); }); @@ -202,70 +217,29 @@ class ConversationCubit extends Cubit> { // Force refresh of conversation keys Future refresh() async { - if (_localConversationCubit != null) { - xxx use defaultdhtrecordcubit refresh mechanism + final lcc = _localConversationCubit; + final rcc = _remoteConversationCubit; + + if (lcc != null) { + await lcc.refreshDefault(); + } + if (rcc != null) { + await rcc.refreshDefault(); } } - // Future readRemoteConversation() async { - // final accountRecordKey = - // _activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey; - // final pool = DHTRecordPool.instance; + Future writeLocalConversation({ + required proto.Conversation conversation, + }) async { + final update = await _localConversationCubit!.record + .tryWriteProtobuf(proto.Conversation.fromBuffer, conversation); - // final crypto = await getConversationCrypto(); - // return (await pool.openRead(_remoteConversationRecordKey!, - // parent: accountRecordKey, crypto: crypto)) - // .scope((remoteConversation) async { - // // - // final conversation = - // await remoteConversation.getProtobuf(proto.Conversation.fromBuffer); - // return conversation; - // }); - // } + if (update != null) { + updateLocalConversationState(AsyncValue.data(conversation)); + } - // Future readLocalConversation() async { - // final accountRecordKey = - // _activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey; - // final pool = DHTRecordPool.instance; - - // final crypto = await getConversationCrypto(); - // return (await pool.openRead(_localConversationRecordKey!, - // parent: accountRecordKey, crypto: crypto)) - // .scope((localConversation) async { - // // - // final update = - // await localConversation.getProtobuf(proto.Conversation.fromBuffer); - // if (update != null) { - // return update; - // } - // return null; - // }); - // } - - // Future writeLocalConversation({ - // required proto.Conversation conversation, - // }) async { - // final accountRecordKey = - // _activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey; - // final pool = DHTRecordPool.instance; - - // final crypto = await getConversationCrypto(); - // final writer = _activeAccountInfo.conversationWriter; - - // return (await pool.openWrite(_localConversationRecordKey!, writer, - // parent: accountRecordKey, crypto: crypto)) - // .scope((localConversation) async { - // // - // final update = await localConversation.tryWriteProtobuf( - // proto.Conversation.fromBuffer, conversation); - // if (update != null) { - // return update; - // } - // return null; - // }); - // } - - // + return update; + } Future getConversationCrypto() async { var conversationCrypto = _conversationCrypto; @@ -286,7 +260,7 @@ class ConversationCubit extends Cubit> { final ActiveAccountInfo _activeAccountInfo; final TypedKey _remoteIdentityPublicKey; TypedKey? _localConversationRecordKey; - TypedKey? _remoteConversationRecordKey; + final TypedKey? _remoteConversationRecordKey; DefaultDHTRecordCubit? _localConversationCubit; DefaultDHTRecordCubit? _remoteConversationCubit; ConversationState _incrementalState; diff --git a/packages/veilid_support/lib/dht_support/src/dht_record_cubit.dart b/packages/veilid_support/lib/dht_support/src/dht_record_cubit.dart index e156ba0..20fdf08 100644 --- a/packages/veilid_support/lib/dht_support/src/dht_record_cubit.dart +++ b/packages/veilid_support/lib/dht_support/src/dht_record_cubit.dart @@ -78,18 +78,29 @@ class DHTRecordCubit extends Cubit> { } Future refresh(List subkeys) async { + var updateSubkeys = [...subkeys]; + for (final skr in subkeys) { for (var sk = skr.low; sk <= skr.high; sk++) { final data = await _record.get( subkey: sk, forceRefresh: true, onlyUpdates: true); if (data != null) { - final newState = await _stateFunction(_record, subkeys, data); - xxx remove sk from update + final newState = await _stateFunction(_record, updateSubkeys, data); + if (newState != null) { + // Emit the new state + emit(AsyncValue.data(newState)); + } + return; } + // remove sk from update list + // if we did not get an update for that subkey + updateSubkeys = updateSubkeys.removeSubkey(sk); } } } + DHTRecord get record => _record; + StreamSubscription? _subscription; late DHTRecord _record; bool _wantsCloseRecord; @@ -113,7 +124,7 @@ class DefaultDHTRecordCubit extends DHTRecordCubit { stateFunction: _makeStateFunction(decodeState), ); - static Future Function(DHTRecord) _makeInitialStateFunction( + static InitialStateFunction _makeInitialStateFunction( T Function(List data) decodeState) => (record) async { final initialData = await record.get(); @@ -123,28 +134,30 @@ class DefaultDHTRecordCubit extends DHTRecordCubit { return decodeState(initialData); }; - static Future Function(DHTRecord, List, Uint8List) - _makeStateFunction(T Function(List data) decodeState) => - (record, subkeys, updatedata) async { - final defaultSubkey = record.subkeyOrDefault(-1); - if (subkeys.containsSubkey(defaultSubkey)) { - final Uint8List data; - final firstSubkey = subkeys.firstOrNull!.low; - if (firstSubkey != defaultSubkey) { - final maybeData = await record.get(forceRefresh: true); - if (maybeData == null) { - return null; - } - data = maybeData; - } else { - data = updatedata; - } - final newState = decodeState(data); - return newState; + static StateFunction _makeStateFunction( + T Function(List data) decodeState) => + (record, subkeys, updatedata) async { + final defaultSubkey = record.subkeyOrDefault(-1); + if (subkeys.containsSubkey(defaultSubkey)) { + final Uint8List data; + final firstSubkey = subkeys.firstOrNull!.low; + if (firstSubkey != defaultSubkey) { + final maybeData = await record.get(forceRefresh: true); + if (maybeData == null) { + return null; } - return null; - }; + data = maybeData; + } else { + data = updatedata; + } + final newState = decodeState(data); + return newState; + } + return null; + }; - // xxx add refresh/get mechanism to DHTRecordCubit and here too, then propagage to conversation_cubit - // xxx should just be a 'get' like in dht_short_array_cubit + Future refreshDefault() async { + final defaultSubkey = _record.subkeyOrDefault(-1); + await refresh([ValueSubkeyRange(low: defaultSubkey, high: defaultSubkey)]); + } } From 09ae8ff6bb7afc05b5085a911c20b589db9c9cc1 Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Thu, 8 Feb 2024 20:35:59 -0500 Subject: [PATCH 28/68] messages work --- lib/chat/cubits/cubits.dart | 1 + lib/chat/cubits/messages_cubit.dart | 209 +++++++++++ lib/chat/views/build_chat_component.dart | 37 -- lib/chat/views/chat_component.dart | 1 + lib/chat/views/views.dart | 4 +- .../cubits/contact_invitation_list_cubit.dart | 7 +- .../models/valid_contact_invitation.dart | 2 +- lib/contacts/contacts.dart | 1 - lib/contacts/cubits/conversation_cubit.dart | 45 +-- lib/contacts/cubits/cubits.dart | 1 + lib/contacts/models/conversation.dart | 345 ------------------ lib/contacts/models/models.dart | 1 - .../home/home_account_ready/chat_only.dart | 2 +- .../home_account_ready.dart | 2 +- .../src/dht_short_array_cubit.dart | 2 - 15 files changed, 246 insertions(+), 414 deletions(-) delete mode 100644 lib/chat/views/build_chat_component.dart delete mode 100644 lib/contacts/models/conversation.dart delete mode 100644 lib/contacts/models/models.dart diff --git a/lib/chat/cubits/cubits.dart b/lib/chat/cubits/cubits.dart index 20763ea..cb7ba44 100644 --- a/lib/chat/cubits/cubits.dart +++ b/lib/chat/cubits/cubits.dart @@ -1 +1,2 @@ export 'active_chat_cubit.dart'; +export 'messages_cubit.dart'; diff --git a/lib/chat/cubits/messages_cubit.dart b/lib/chat/cubits/messages_cubit.dart index e69de29..76acfdc 100644 --- a/lib/chat/cubits/messages_cubit.dart +++ b/lib/chat/cubits/messages_cubit.dart @@ -0,0 +1,209 @@ +import 'dart:async'; + +import 'package:fast_immutable_collections/fast_immutable_collections.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:veilid_support/veilid_support.dart'; + +import '../../account_manager/account_manager.dart'; +import '../../proto/proto.dart' as proto; + +class _MessageQueueEntry { + _MessageQueueEntry( + {required this.localMessages, required this.remoteMessages}); + IList localMessages; + IList remoteMessages; +} + +class MessagesCubit extends Cubit>> { + MessagesCubit( + {required ActiveAccountInfo activeAccountInfo, + required TypedKey remoteIdentityPublicKey, + required TypedKey localConversationRecordKey, + required TypedKey localMessagesRecordKey, + required TypedKey remoteConversationRecordKey, + required TypedKey remoteMessagesRecordKey}) + : _activeAccountInfo = activeAccountInfo, + _localMessagesRecordKey = localMessagesRecordKey, + _remoteIdentityPublicKey = remoteIdentityPublicKey, + _remoteMessagesRecordKey = remoteMessagesRecordKey, + _remoteMessagesQueue = StreamController(), + super(const AsyncValue.loading()) { + // Local messages key + Future.delayed(Duration.zero, () async { + final crypto = await getMessagesCrypto(); + final writer = _activeAccountInfo.conversationWriter; + final record = await DHTShortArray.openWrite( + _localMessagesRecordKey, writer, + parent: localConversationRecordKey, crypto: crypto); + await _setLocalMessages(record); + }); + + // Remote messages key + Future.delayed(Duration.zero, () async { + // Open remote record key if it is specified + final crypto = await getMessagesCrypto(); + final record = await DHTShortArray.openRead(_remoteMessagesRecordKey, + parent: remoteConversationRecordKey, crypto: crypto); + await _setRemoteMessages(record); + }); + + // Remote messages listener + Future.delayed(Duration.zero, () async { + await for (final entry in _remoteMessagesQueue.stream) { + await _updateRemoteMessagesStateAsync(entry); + } + }); + } + + @override + Future close() async { + await super.close(); + } + + void updateLocalMessagesState(AsyncValue> avmessages) { + // Updated local messages from online just update the state immediately + emit(avmessages); + } + + Future _updateRemoteMessagesStateAsync(_MessageQueueEntry entry) async { + // Updated remote messages need to be merged with the local messages state + + // Ensure remoteMessages is sorted by timestamp + final remoteMessages = + entry.remoteMessages.sort((a, b) => a.timestamp.compareTo(b.timestamp)); + + // Existing messages will always be sorted by timestamp so merging is easy + var localMessages = entry.localMessages; + var pos = 0; + for (final newMessage in remoteMessages) { + var skip = false; + while (pos < localMessages.length) { + final m = localMessages[pos]; + + // If timestamp to insert is less than + // the current position, insert it here + final newTs = Timestamp.fromInt64(newMessage.timestamp); + final curTs = Timestamp.fromInt64(m.timestamp); + final cmp = newTs.compareTo(curTs); + if (cmp < 0) { + break; + } else if (cmp == 0) { + skip = true; + break; + } + pos++; + } + // Insert at this position + if (!skip) { + // Insert into dht backing array + await _localMessagesCubit!.shortArray + .tryInsertItem(pos, newMessage.writeToBuffer()); + // Insert into local copy as well for this operation + localMessages = localMessages.insert(pos, newMessage); + } + } + } + + void updateRemoteMessagesState(AsyncValue> avmessages) { + final remoteMessages = avmessages.data?.value; + if (remoteMessages == null) { + return; + } + + final localMessages = state.data?.value; + if (localMessages == null) { + // No local messages means remote messages + // are all we have so merging is easy + emit(AsyncValue.data(remoteMessages)); + return; + } + + _remoteMessagesQueue.add(_MessageQueueEntry( + localMessages: localMessages, remoteMessages: remoteMessages)); + } + + // Open local messages key + Future _setLocalMessages(DHTShortArray localMessagesRecord) async { + assert(_localMessagesCubit == null, 'shoud not set local messages twice'); + _localMessagesCubit = DHTShortArrayCubit.value( + shortArray: localMessagesRecord, + decodeElement: proto.Message.fromBuffer); + _localMessagesCubit!.stream.listen(updateLocalMessagesState); + } + + // Open remote messages key + Future _setRemoteMessages(DHTShortArray remoteMessagesRecord) async { + assert(_remoteMessagesCubit == null, 'shoud not set remote messages twice'); + _remoteMessagesCubit = DHTShortArrayCubit.value( + shortArray: remoteMessagesRecord, + decodeElement: proto.Message.fromBuffer); + _remoteMessagesCubit!.stream.listen(updateRemoteMessagesState); + } + + // Initialize local messages + static Future initLocalMessages({ + required ActiveAccountInfo activeAccountInfo, + required TypedKey remoteIdentityPublicKey, + required TypedKey localConversationKey, + required FutureOr Function(DHTShortArray) callback, + }) async { + final crypto = + await _makeMessagesCrypto(activeAccountInfo, remoteIdentityPublicKey); + final writer = activeAccountInfo.conversationWriter; + + return (await DHTShortArray.create( + parent: localConversationKey, crypto: crypto, smplWriter: writer)) + .deleteScope((messages) async => await callback(messages)); + } + + // Force refresh of messages + Future refresh() async { + final lcc = _localMessagesCubit; + final rcc = _remoteMessagesCubit; + + if (lcc != null) { + await lcc.refresh(); + } + if (rcc != null) { + await rcc.refresh(); + } + } + + Future addMessage({required proto.Message message}) async { + await _localMessagesCubit!.shortArray.tryAddItem(message.writeToBuffer()); + } + + Future getMessagesCrypto() async { + var messagesCrypto = _messagesCrypto; + if (messagesCrypto != null) { + return messagesCrypto; + } + messagesCrypto = + await _makeMessagesCrypto(_activeAccountInfo, _remoteIdentityPublicKey); + _messagesCrypto = messagesCrypto; + return messagesCrypto; + } + + static Future _makeMessagesCrypto( + ActiveAccountInfo activeAccountInfo, + TypedKey remoteIdentityPublicKey) async { + final identitySecret = activeAccountInfo.userLogin.identitySecret; + final cs = await Veilid.instance.getCryptoSystem(identitySecret.kind); + final sharedSecret = + await cs.cachedDH(remoteIdentityPublicKey.value, identitySecret.value); + + final messagesCrypto = await DHTRecordCryptoPrivate.fromSecret( + identitySecret.kind, sharedSecret); + return messagesCrypto; + } + + final ActiveAccountInfo _activeAccountInfo; + final TypedKey _remoteIdentityPublicKey; + final TypedKey _localMessagesRecordKey; + final TypedKey _remoteMessagesRecordKey; + DHTShortArrayCubit? _localMessagesCubit; + DHTShortArrayCubit? _remoteMessagesCubit; + final StreamController<_MessageQueueEntry> _remoteMessagesQueue; + // + DHTRecordCrypto? _messagesCrypto; +} diff --git a/lib/chat/views/build_chat_component.dart b/lib/chat/views/build_chat_component.dart deleted file mode 100644 index aa74ff2..0000000 --- a/lib/chat/views/build_chat_component.dart +++ /dev/null @@ -1,37 +0,0 @@ -import 'package:flutter/material.dart'; - -import '../../tools/tools.dart'; - -Widget buildChatComponent() { - // final contactList = ref.watch(fetchContactListProvider).asData?.value ?? - // const IListConst([]); - - // final activeChat = ref.watch(activeChatStateProvider); - // if (activeChat == null) { - // return const EmptyChatWidget(); - // } - - // final activeAccountInfo = - // ref.watch(fetchActiveAccountProvider).asData?.value; - // if (activeAccountInfo == null) { - // return const EmptyChatWidget(); - // } - - // final activeChatContactIdx = contactList.indexWhere( - // (c) => - // proto.TypedKeyProto.fromProto(c.remoteConversationRecordKey) == - // activeChat, - // ); - // if (activeChatContactIdx == -1) { - // ref.read(activeChatStateProvider.notifier).state = null; - // return const EmptyChatWidget(); - // } - // final activeChatContact = contactList[activeChatContactIdx]; - - // return ChatComponent( - // activeAccountInfo: activeAccountInfo, - // activeChat: activeChat, - // activeChatContact: activeChatContact); - // } - return Builder(builder: waitingPage); -} diff --git a/lib/chat/views/chat_component.dart b/lib/chat/views/chat_component.dart index dcfc8a1..62ca94f 100644 --- a/lib/chat/views/chat_component.dart +++ b/lib/chat/views/chat_component.dart @@ -6,6 +6,7 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_chat_types/flutter_chat_types.dart' as types; import 'package:flutter_chat_ui/flutter_chat_ui.dart'; +import '../../proto/proto.dart' as proto; import '../../theme/theme.dart'; import '../chat.dart'; diff --git a/lib/chat/views/views.dart b/lib/chat/views/views.dart index b63153b..6230e65 100644 --- a/lib/chat/views/views.dart +++ b/lib/chat/views/views.dart @@ -1 +1,3 @@ -export 'build_chat_component.dart'; +export 'chat_component.dart'; +export 'empty_chat_widget.dart'; +export 'no_conversation_widget.dart'; diff --git a/lib/contact_invitation/cubits/contact_invitation_list_cubit.dart b/lib/contact_invitation/cubits/contact_invitation_list_cubit.dart index 60f2e74..6ff1c8a 100644 --- a/lib/contact_invitation/cubits/contact_invitation_list_cubit.dart +++ b/lib/contact_invitation/cubits/contact_invitation_list_cubit.dart @@ -330,17 +330,20 @@ class ContactInvitationListCubit final remoteConversationRecordKey = proto.TypedKeyProto.fromProto( contactResponse.remoteConversationRecordKey); - final conversation = ConversationManager( + final conversation = ConversationCubit( activeAccountInfo: _activeAccountInfo, remoteIdentityPublicKey: contactIdentityMaster.identityPublicTypedKey(), remoteConversationRecordKey: remoteConversationRecordKey); + await conversation.refresh(); - final remoteConversation = await conversation.readRemoteConversation(); + final remoteConversation = + conversation.state.data?.value.remoteConversation; if (remoteConversation == null) { log.info('Remote conversation could not be read. Waiting...'); return null; } + // Complete the local conversation now that we have the remote profile final localConversationRecordKey = proto.TypedKeyProto.fromProto( contactInvitationRecord.localConversationRecordKey); diff --git a/lib/contact_invitation/models/valid_contact_invitation.dart b/lib/contact_invitation/models/valid_contact_invitation.dart index 3205c23..67e509a 100644 --- a/lib/contact_invitation/models/valid_contact_invitation.dart +++ b/lib/contact_invitation/models/valid_contact_invitation.dart @@ -42,7 +42,7 @@ class ValidContactInvitation { .maybeDeleteScope(!isSelf, (contactRequestInbox) async { // Create local conversation key for this // contact and send via contact response - final conversation = ConversationManager( + final conversation = ConversationCubit( activeAccountInfo: _activeAccountInfo, remoteIdentityPublicKey: _contactIdentityMaster.identityPublicTypedKey()); diff --git a/lib/contacts/contacts.dart b/lib/contacts/contacts.dart index 08ae2e7..6acdd43 100644 --- a/lib/contacts/contacts.dart +++ b/lib/contacts/contacts.dart @@ -1,3 +1,2 @@ export 'cubits/cubits.dart'; -export 'models/models.dart'; export 'views/views.dart'; diff --git a/lib/contacts/cubits/conversation_cubit.dart b/lib/contacts/cubits/conversation_cubit.dart index a260ad6..9590a08 100644 --- a/lib/contacts/cubits/conversation_cubit.dart +++ b/lib/contacts/cubits/conversation_cubit.dart @@ -11,6 +11,7 @@ import 'package:meta/meta.dart'; import 'package:veilid_support/veilid_support.dart'; import '../../account_manager/account_manager.dart'; +import '../../chat/chat.dart'; import '../../proto/proto.dart' as proto; @immutable @@ -181,31 +182,31 @@ class ConversationCubit extends Cubit> { // ignore: prefer_expression_function_bodies .deleteScope((localConversation) async { // Make messages log - return (await DHTShortArray.create( - parent: localConversation.key, - crypto: crypto, - smplWriter: writer)) - .deleteScope((messages) async { - // Create initial local conversation key contents - final conversation = proto.Conversation() - ..profile = profile - ..identityMasterJson = jsonEncode( - _activeAccountInfo.localAccount.identityMaster.toJson()) - ..messages = messages.record.key.toProto(); + return MessagesCubit.initLocalMessages( + activeAccountInfo: _activeAccountInfo, + remoteIdentityPublicKey: _remoteIdentityPublicKey, + localConversationKey: localConversation.key, + callback: (messages) async { + // Create initial local conversation key contents + final conversation = proto.Conversation() + ..profile = profile + ..identityMasterJson = jsonEncode( + _activeAccountInfo.localAccount.identityMaster.toJson()) + ..messages = messages.record.key.toProto(); - // Write initial conversation to record - final update = await localConversation.tryWriteProtobuf( - proto.Conversation.fromBuffer, conversation); - if (update != null) { - throw Exception('Failed to write local conversation'); - } - final out = await callback(localConversation); + // Write initial conversation to record + final update = await localConversation.tryWriteProtobuf( + proto.Conversation.fromBuffer, conversation); + if (update != null) { + throw Exception('Failed to write local conversation'); + } + final out = await callback(localConversation); - // Upon success emit the local conversation record to the state - updateLocalConversationState(AsyncValue.data(conversation)); + // Upon success emit the local conversation record to the state + updateLocalConversationState(AsyncValue.data(conversation)); - return out; - }); + return out; + }); }); // If success, save the new local conversation record key in this object diff --git a/lib/contacts/cubits/cubits.dart b/lib/contacts/cubits/cubits.dart index 795d497..3d16d52 100644 --- a/lib/contacts/cubits/cubits.dart +++ b/lib/contacts/cubits/cubits.dart @@ -1 +1,2 @@ export 'contact_list_cubit.dart'; +export 'conversation_cubit.dart'; diff --git a/lib/contacts/models/conversation.dart b/lib/contacts/models/conversation.dart deleted file mode 100644 index 4e6d712..0000000 --- a/lib/contacts/models/conversation.dart +++ /dev/null @@ -1,345 +0,0 @@ -// A Conversation is a type of Chat that is 1:1 between two Contacts only -// Each Contact in the ContactList has at most one Conversation between the -// remote contact and the local account - -import 'dart:async'; -import 'dart:convert'; - -import 'package:fast_immutable_collections/fast_immutable_collections.dart'; -import 'package:veilid_support/veilid_support.dart'; - -import '../../account_manager/account_manager.dart'; -import '../../proto/proto.dart' as proto; -import '../../tools/tools.dart'; - -class ConversationManager { - ConversationManager( - {required ActiveAccountInfo activeAccountInfo, - required TypedKey remoteIdentityPublicKey, - TypedKey? localConversationRecordKey, - TypedKey? remoteConversationRecordKey}) - : _activeAccountInfo = activeAccountInfo, - _localConversationRecordKey = localConversationRecordKey, - _remoteIdentityPublicKey = remoteIdentityPublicKey, - _remoteConversationRecordKey = remoteConversationRecordKey; - - // Initialize a local conversation - // If we were the initiator of the conversation there may be an - // incomplete 'existingConversationRecord' that we need to fill - // in now that we have the remote identity key - Future initLocalConversation( - {required proto.Profile profile, - required FutureOr Function(DHTRecord) callback, - TypedKey? existingConversationRecordKey}) async { - final pool = DHTRecordPool.instance; - final accountRecordKey = - _activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey; - - final crypto = await getConversationCrypto(); - final writer = _activeAccountInfo.conversationWriter; - - // Open with SMPL scheme for identity writer - late final DHTRecord localConversationRecord; - if (existingConversationRecordKey != null) { - localConversationRecord = await pool.openWrite( - existingConversationRecordKey, writer, - parent: accountRecordKey, crypto: crypto); - } else { - final localConversationRecordCreate = await pool.create( - parent: accountRecordKey, - crypto: crypto, - schema: DHTSchema.smpl( - oCnt: 0, members: [DHTSchemaMember(mKey: writer.key, mCnt: 1)])); - await localConversationRecordCreate.close(); - localConversationRecord = await pool.openWrite( - localConversationRecordCreate.key, writer, - parent: accountRecordKey, crypto: crypto); - } - final out = localConversationRecord - // ignore: prefer_expression_function_bodies - .deleteScope((localConversation) async { - // Make messages log - return (await DHTShortArray.create( - parent: localConversation.key, - crypto: crypto, - smplWriter: writer)) - .deleteScope((messages) async { - // Write local conversation key - final conversation = proto.Conversation() - ..profile = profile - ..identityMasterJson = jsonEncode( - _activeAccountInfo.localAccount.identityMaster.toJson()) - ..messages = messages.record.key.toProto(); - - // - final update = await localConversation.tryWriteProtobuf( - proto.Conversation.fromBuffer, conversation); - if (update != null) { - throw Exception('Failed to write local conversation'); - } - return await callback(localConversation); - }); - }); - // If success, save the new local conversation record key in this object - _localConversationRecordKey = localConversationRecord.key; - return out; - } - - Future readRemoteConversation() async { - final accountRecordKey = - _activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey; - final pool = DHTRecordPool.instance; - - final crypto = await getConversationCrypto(); - return (await pool.openRead(_remoteConversationRecordKey!, - parent: accountRecordKey, crypto: crypto)) - .scope((remoteConversation) async { - // - final conversation = - await remoteConversation.getProtobuf(proto.Conversation.fromBuffer); - return conversation; - }); - } - - Future readLocalConversation() async { - final accountRecordKey = - _activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey; - final pool = DHTRecordPool.instance; - - final crypto = await getConversationCrypto(); - return (await pool.openRead(_localConversationRecordKey!, - parent: accountRecordKey, crypto: crypto)) - .scope((localConversation) async { - // - final update = - await localConversation.getProtobuf(proto.Conversation.fromBuffer); - if (update != null) { - return update; - } - return null; - }); - } - - Future writeLocalConversation({ - required proto.Conversation conversation, - }) async { - final accountRecordKey = - _activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey; - final pool = DHTRecordPool.instance; - - final crypto = await getConversationCrypto(); - final writer = _activeAccountInfo.conversationWriter; - - return (await pool.openWrite(_localConversationRecordKey!, writer, - parent: accountRecordKey, crypto: crypto)) - .scope((localConversation) async { - // - final update = await localConversation.tryWriteProtobuf( - proto.Conversation.fromBuffer, conversation); - if (update != null) { - return update; - } - return null; - }); - } - - Future addLocalConversationMessage( - {required proto.Message message}) async { - final conversation = await readLocalConversation(); - if (conversation == null) { - return; - } - final messagesRecordKey = - proto.TypedKeyProto.fromProto(conversation.messages); - final crypto = await getConversationCrypto(); - final writer = _activeAccountInfo.conversationWriter; - - await (await DHTShortArray.openWrite(messagesRecordKey, writer, - parent: _localConversationRecordKey, crypto: crypto)) - .scope((messages) async { - await messages.tryAddItem(message.writeToBuffer()); - }); - } - - Future mergeLocalConversationMessages( - {required IList newMessages}) async { - final conversation = await readLocalConversation(); - if (conversation == null) { - return false; - } - var changed = false; - final messagesRecordKey = - proto.TypedKeyProto.fromProto(conversation.messages); - final crypto = await getConversationCrypto(); - final writer = _activeAccountInfo.conversationWriter; - - newMessages = newMessages.sort((a, b) => Timestamp.fromInt64(a.timestamp) - .compareTo(Timestamp.fromInt64(b.timestamp))); - - await (await DHTShortArray.openWrite(messagesRecordKey, writer, - parent: _localConversationRecordKey, crypto: crypto)) - .scope((messages) async { - // Ensure newMessages is sorted by timestamp - newMessages = - newMessages.sort((a, b) => a.timestamp.compareTo(b.timestamp)); - - // Existing messages will always be sorted by timestamp so merging is easy - var pos = 0; - outer: - for (final newMessage in newMessages) { - var skip = false; - while (pos < messages.length) { - final m = - await messages.getItemProtobuf(proto.Message.fromBuffer, pos); - if (m == null) { - log.error('unable to get message #$pos'); - break outer; - } - - // If timestamp to insert is less than - // the current position, insert it here - final newTs = Timestamp.fromInt64(newMessage.timestamp); - final curTs = Timestamp.fromInt64(m.timestamp); - final cmp = newTs.compareTo(curTs); - if (cmp < 0) { - break; - } else if (cmp == 0) { - skip = true; - break; - } - pos++; - } - // Insert at this position - if (!skip) { - await messages.tryInsertItem(pos, newMessage.writeToBuffer()); - changed = true; - } - } - }); - return changed; - } - - Future?> getRemoteConversationMessages() async { - final conversation = await readRemoteConversation(); - if (conversation == null) { - return null; - } - final messagesRecordKey = - proto.TypedKeyProto.fromProto(conversation.messages); - final crypto = await getConversationCrypto(); - - return (await DHTShortArray.openRead(messagesRecordKey, - parent: _remoteConversationRecordKey, crypto: crypto)) - .scope((messages) async { - var out = IList(); - for (var i = 0; i < messages.length; i++) { - final msg = await messages.getItemProtobuf(proto.Message.fromBuffer, i); - if (msg == null) { - throw Exception('Failed to get message'); - } - out = out.add(msg); - } - return out; - }); - } - - // - - Future getConversationCrypto() async { - var conversationCrypto = _conversationCrypto; - if (conversationCrypto != null) { - return conversationCrypto; - } - final identitySecret = _activeAccountInfo.userLogin.identitySecret; - final cs = await Veilid.instance.getCryptoSystem(identitySecret.kind); - final sharedSecret = - await cs.cachedDH(_remoteIdentityPublicKey.value, identitySecret.value); - - conversationCrypto = await DHTRecordCryptoPrivate.fromSecret( - identitySecret.kind, sharedSecret); - _conversationCrypto = conversationCrypto; - return conversationCrypto; - } - - Future?> getLocalConversationMessages() async { - final conversation = await readLocalConversation(); - if (conversation == null) { - return null; - } - final messagesRecordKey = - proto.TypedKeyProto.fromProto(conversation.messages); - final crypto = await getConversationCrypto(); - - return (await DHTShortArray.openRead(messagesRecordKey, - parent: _localConversationRecordKey, crypto: crypto)) - .scope((messages) async { - var out = IList(); - for (var i = 0; i < messages.length; i++) { - final msg = await messages.getItemProtobuf(proto.Message.fromBuffer, i); - if (msg == null) { - throw Exception('Failed to get message'); - } - out = out.add(msg); - } - return out; - }); - } - - final ActiveAccountInfo _activeAccountInfo; - final TypedKey _remoteIdentityPublicKey; - TypedKey? _localConversationRecordKey; - TypedKey? _remoteConversationRecordKey; - // - DHTRecordCrypto? _conversationCrypto; -} - - -// // -// // -// // -// // - -// @riverpod -// class ActiveConversationMessages extends _$ActiveConversationMessages { -// /// Get message for active conversation -// @override -// FutureOr?> build() async { -// await eventualVeilid.future; - -// final activeChat = ref.watch(activeChatStateProvider); -// if (activeChat == null) { -// return null; -// } - -// final activeAccountInfo = -// await ref.watch(fetchActiveAccountProvider.future); -// if (activeAccountInfo == null) { -// return null; -// } - -// final contactList = ref.watch(fetchContactListProvider).asData?.value ?? -// const IListConst([]); - -// final activeChatContactIdx = contactList.indexWhere( -// (c) => -// proto.TypedKeyProto.fromProto(c.remoteConversationRecordKey) == -// activeChat, -// ); -// if (activeChatContactIdx == -1) { -// return null; -// } -// final activeChatContact = contactList[activeChatContactIdx]; -// final remoteIdentityPublicKey = -// proto.TypedKeyProto.fromProto(activeChatContact.identityPublicKey); -// // final remoteConversationRecordKey = proto.TypedKeyProto.fromProto( -// // activeChatContact.remoteConversationRecordKey); -// final localConversationRecordKey = proto.TypedKeyProto.fromProto( -// activeChatContact.localConversationRecordKey); - -// return await getLocalConversationMessages( -// activeAccountInfo: activeAccountInfo, -// localConversationRecordKey: localConversationRecordKey, -// remoteIdentityPublicKey: remoteIdentityPublicKey, -// ); -// } -// } diff --git a/lib/contacts/models/models.dart b/lib/contacts/models/models.dart deleted file mode 100644 index 4a9d767..0000000 --- a/lib/contacts/models/models.dart +++ /dev/null @@ -1 +0,0 @@ -export 'conversation.dart'; diff --git a/lib/layout/home/home_account_ready/chat_only.dart b/lib/layout/home/home_account_ready/chat_only.dart index 48cb02e..9e89b40 100644 --- a/lib/layout/home/home_account_ready/chat_only.dart +++ b/lib/layout/home/home_account_ready/chat_only.dart @@ -35,6 +35,6 @@ class ChatOnlyPageState extends State Widget build(BuildContext context) => SafeArea( child: GestureDetector( onTap: () => FocusScope.of(context).requestFocus(_unfocusNode), - child: buildChatComponent(), + child: const ChatComponent(), )); } diff --git a/lib/layout/home/home_account_ready/home_account_ready.dart b/lib/layout/home/home_account_ready/home_account_ready.dart index 1d900b3..6a42f97 100644 --- a/lib/layout/home/home_account_ready/home_account_ready.dart +++ b/lib/layout/home/home_account_ready/home_account_ready.dart @@ -73,7 +73,7 @@ class HomeAccountReadyState extends State builder: (context) => Material(color: Colors.transparent, child: buildUserPanel())); - Widget buildTabletRightPane(BuildContext context) => buildChatComponent(); + Widget buildTabletRightPane(BuildContext context) => const ChatComponent(); // ignore: prefer_expression_function_bodies Widget buildTablet(BuildContext context) { diff --git a/packages/veilid_support/lib/dht_support/src/dht_short_array_cubit.dart b/packages/veilid_support/lib/dht_support/src/dht_short_array_cubit.dart index d827500..71d1d38 100644 --- a/packages/veilid_support/lib/dht_support/src/dht_short_array_cubit.dart +++ b/packages/veilid_support/lib/dht_support/src/dht_short_array_cubit.dart @@ -2,7 +2,6 @@ import 'dart:async'; import 'package:bloc/bloc.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; -import 'package:meta/meta.dart'; import '../../veilid_support.dart'; @@ -105,7 +104,6 @@ class DHTShortArrayCubit extends Cubit>> { await super.close(); } - @protected DHTShortArray get shortArray => _shortArray; late final DHTShortArray _shortArray; From aa376a449d551fabab8a419e7e0ab81c0ff4fe9f Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Thu, 8 Feb 2024 21:05:59 -0500 Subject: [PATCH 29/68] checkpoint --- lib/chat/cubits/active_chat_cubit.dart | 4 +- lib/chat/views/chat_component.dart | 153 ++++++++++++--------- lib/chat/views/no_conversation_widget.dart | 5 +- 3 files changed, 95 insertions(+), 67 deletions(-) diff --git a/lib/chat/cubits/active_chat_cubit.dart b/lib/chat/cubits/active_chat_cubit.dart index d8cdc3e..e47caec 100644 --- a/lib/chat/cubits/active_chat_cubit.dart +++ b/lib/chat/cubits/active_chat_cubit.dart @@ -4,7 +4,7 @@ import 'package:veilid_support/veilid_support.dart'; class ActiveChatCubit extends Cubit { ActiveChatCubit(super.initialState); - void setActiveChat(TypedKey? activeChat) { - emit(activeChat); + void setActiveChat(TypedKey? activeChatRemoteConversationRecordKey) { + emit(activeChatRemoteConversationRecordKey); } } diff --git a/lib/chat/views/chat_component.dart b/lib/chat/views/chat_component.dart index 62ca94f..dd6aa65 100644 --- a/lib/chat/views/chat_component.dart +++ b/lib/chat/views/chat_component.dart @@ -6,8 +6,11 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_chat_types/flutter_chat_types.dart' as types; import 'package:flutter_chat_ui/flutter_chat_ui.dart'; +import '../../account_manager/account_manager.dart'; +import '../../contacts/contacts.dart'; import '../../proto/proto.dart' as proto; import '../../theme/theme.dart'; +import '../../tools/tools.dart'; import '../chat.dart'; class ChatComponent extends StatefulWidget { @@ -109,73 +112,99 @@ class ChatComponentState extends State { final scale = theme.extension()!; final textTheme = Theme.of(context).textTheme; final chatTheme = makeChatTheme(scale, textTheme); - final contactName = widget.activeChatContact.editedProfile.name; - final protoMessages = - ref.watch(activeConversationMessagesProvider).asData?.value; - if (protoMessages == null) { - return waitingPage(context); - } - final messages = []; - for (final protoMessage in protoMessages) { - final message = protoMessageToMessage(protoMessage); - messages.insert(0, message); - } + final activeChatCubit = context.watch(); + final contactListCubit = context.watch(); - return DefaultTextStyle( - style: textTheme.bodySmall!, - child: Align( - alignment: AlignmentDirectional.centerEnd, - child: Stack( - children: [ - Column( - children: [ - Container( - height: 48, - decoration: BoxDecoration( - color: scale.primaryScale.subtleBorder, + final activeChatContactKey = activeChatCubit.state; + if (activeChatContactKey == null) { + return const NoConversationWidget(); + } + return contactListCubit.state.builder((context, contactList) { + + // Get active chat contact profile + final activeChatContactIdx = contactList.indexWhere( + (c) => activeChatContactKey == c.remoteConversationRecordKey); + late final proto.Contact activeChatContact; + if (activeChatContactIdx == -1) { + activeChatCubit.setActiveChat(null); + return const NoConversationWidget(); + } else { + activeChatContact = contactList[activeChatContactIdx]; + } + final contactName = activeChatContact.editedProfile.name; + + // Make a messages cubit for this conversation + xxx + + final protoMessages = + ref.watch(activeConversationMessagesProvider).asData?.value; + if (protoMessages == null) { + return waitingPage(context); + } + final messages = []; + for (final protoMessage in protoMessages) { + final message = protoMessageToMessage(protoMessage); + messages.insert(0, message); + } + + return DefaultTextStyle( + style: textTheme.bodySmall!, + child: Align( + alignment: AlignmentDirectional.centerEnd, + child: Stack( + children: [ + Column( + children: [ + Container( + height: 48, + decoration: BoxDecoration( + color: scale.primaryScale.subtleBorder, + ), + child: Row(children: [ + Align( + alignment: AlignmentDirectional.centerStart, + child: Padding( + padding: const EdgeInsetsDirectional.fromSTEB( + 16, 0, 16, 0), + child: Text(contactName, + textAlign: TextAlign.start, + style: textTheme.titleMedium), + )), + const Spacer(), + IconButton( + icon: const Icon(Icons.close), + onPressed: () async { + context + .read() + .setActiveChat(null); + }).paddingLTRB(16, 0, 16, 0) + ]), ), - child: Row(children: [ - Align( - alignment: AlignmentDirectional.centerStart, - child: Padding( - padding: const EdgeInsetsDirectional.fromSTEB( - 16, 0, 16, 0), - child: Text(contactName, - textAlign: TextAlign.start, - style: textTheme.titleMedium), - )), - const Spacer(), - IconButton( - icon: const Icon(Icons.close), - onPressed: () async { - context.read().setActiveChat(null); - }).paddingLTRB(16, 0, 16, 0) - ]), - ), - Expanded( - child: DecoratedBox( - decoration: const BoxDecoration(), - child: Chat( - theme: chatTheme, - messages: messages, - //onAttachmentPressed: _handleAttachmentPressed, - //onMessageTap: _handleMessageTap, - //onPreviewDataFetched: _handlePreviewDataFetched, + Expanded( + child: DecoratedBox( + decoration: const BoxDecoration(), + child: Chat( + theme: chatTheme, + messages: messages, + //onAttachmentPressed: _handleAttachmentPressed, + //onMessageTap: _handleMessageTap, + //onPreviewDataFetched: _handlePreviewDataFetched, - onSendPressed: (message) { - unawaited(_handleSendPressed(message)); - }, - //showUserAvatars: false, - //showUserNames: true, - user: _localUser, + onSendPressed: (message) { + unawaited(_handleSendPressed(message)); + }, + //showUserAvatars: false, + //showUserNames: true, + user: _localUser, + ), ), ), - ), - ], - ), - ], - ), - )); + ], + ), + ], + ), + )); + }); } } diff --git a/lib/chat/views/no_conversation_widget.dart b/lib/chat/views/no_conversation_widget.dart index 4657966..1b8545f 100644 --- a/lib/chat/views/no_conversation_widget.dart +++ b/lib/chat/views/no_conversation_widget.dart @@ -1,8 +1,7 @@ import 'package:flutter/material.dart'; -//XXX should rename this -class NoContactWidget extends StatelessWidget { - const NoContactWidget({super.key}); +class NoConversationWidget extends StatelessWidget { + const NoConversationWidget({super.key}); @override // ignore: prefer_expression_function_bodies From 43dbf26cc02f854d1c7280ec54e7725014761142 Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Fri, 9 Feb 2024 21:17:28 -0500 Subject: [PATCH 30/68] active conversations cubit and blocmapcubit --- lib/chat/views/chat_component.dart | 135 +++++++++--------- .../cubits/active_conversations_cubit.dart | 24 ++++ lib/chat_list/cubits/cubits.dart | 1 + .../chat_single_contact_item_widget.dart | 4 + .../home_account_ready.dart | 8 ++ lib/tools/bloc_map_cubit.dart | 96 +++++++++++++ lib/tools/stream_wrapper_cubit.dart | 10 +- lib/tools/tools.dart | 1 + .../lib/src/async_tag_lock.dart | 9 ++ 9 files changed, 216 insertions(+), 72 deletions(-) create mode 100644 lib/chat_list/cubits/active_conversations_cubit.dart create mode 100644 lib/tools/bloc_map_cubit.dart diff --git a/lib/chat/views/chat_component.dart b/lib/chat/views/chat_component.dart index dd6aa65..71cec22 100644 --- a/lib/chat/views/chat_component.dart +++ b/lib/chat/views/chat_component.dart @@ -115,96 +115,97 @@ class ChatComponentState extends State { final activeChatCubit = context.watch(); final contactListCubit = context.watch(); + final activeAccountInfo = context.watch(); final activeChatContactKey = activeChatCubit.state; if (activeChatContactKey == null) { return const NoConversationWidget(); } return contactListCubit.state.builder((context, contactList) { - // Get active chat contact profile final activeChatContactIdx = contactList.indexWhere( (c) => activeChatContactKey == c.remoteConversationRecordKey); late final proto.Contact activeChatContact; if (activeChatContactIdx == -1) { - activeChatCubit.setActiveChat(null); return const NoConversationWidget(); } else { activeChatContact = contactList[activeChatContactIdx]; } final contactName = activeChatContact.editedProfile.name; - // Make a messages cubit for this conversation - xxx + // final protoMessages = + // ref.watch(activeConversationMessagesProvider).asData?.value; + // if (protoMessages == null) { + // return waitingPage(context); + // } + // final messages = []; + // for (final protoMessage in protoMessages) { + // final message = protoMessageToMessage(protoMessage); + // messages.insert(0, message); + // } - final protoMessages = - ref.watch(activeConversationMessagesProvider).asData?.value; - if (protoMessages == null) { - return waitingPage(context); - } - final messages = []; - for (final protoMessage in protoMessages) { - final message = protoMessageToMessage(protoMessage); - messages.insert(0, message); - } - - return DefaultTextStyle( - style: textTheme.bodySmall!, - child: Align( - alignment: AlignmentDirectional.centerEnd, - child: Stack( - children: [ - Column( + return BlocProvider( + create: (context) => MessagesCubit( + activeAccountInfo: activeAccountInfo, + remoteIdentityPublicKey: activeChatContact.identityPublicKey, localConversationRecordKey: activeChatContact.localConversationRecordKey, localMessagesRecordKey: activeChatContact., + ), + child: DefaultTextStyle( + style: textTheme.bodySmall!, + child: Align( + alignment: AlignmentDirectional.centerEnd, + child: Stack( children: [ - Container( - height: 48, - decoration: BoxDecoration( - color: scale.primaryScale.subtleBorder, - ), - child: Row(children: [ - Align( - alignment: AlignmentDirectional.centerStart, - child: Padding( - padding: const EdgeInsetsDirectional.fromSTEB( - 16, 0, 16, 0), - child: Text(contactName, - textAlign: TextAlign.start, - style: textTheme.titleMedium), - )), - const Spacer(), - IconButton( - icon: const Icon(Icons.close), - onPressed: () async { - context - .read() - .setActiveChat(null); - }).paddingLTRB(16, 0, 16, 0) - ]), - ), - Expanded( - child: DecoratedBox( - decoration: const BoxDecoration(), - child: Chat( - theme: chatTheme, - messages: messages, - //onAttachmentPressed: _handleAttachmentPressed, - //onMessageTap: _handleMessageTap, - //onPreviewDataFetched: _handlePreviewDataFetched, - - onSendPressed: (message) { - unawaited(_handleSendPressed(message)); - }, - //showUserAvatars: false, - //showUserNames: true, - user: _localUser, + Column( + children: [ + Container( + height: 48, + decoration: BoxDecoration( + color: scale.primaryScale.subtleBorder, + ), + child: Row(children: [ + Align( + alignment: AlignmentDirectional.centerStart, + child: Padding( + padding: const EdgeInsetsDirectional.fromSTEB( + 16, 0, 16, 0), + child: Text(contactName, + textAlign: TextAlign.start, + style: textTheme.titleMedium), + )), + const Spacer(), + IconButton( + icon: const Icon(Icons.close), + onPressed: () async { + context + .read() + .setActiveChat(null); + }).paddingLTRB(16, 0, 16, 0) + ]), ), - ), + Expanded( + child: DecoratedBox( + decoration: const BoxDecoration(), + child: Chat( + theme: chatTheme, + messages: messages, + //onAttachmentPressed: _handleAttachmentPressed, + //onMessageTap: _handleMessageTap, + //onPreviewDataFetched: _handlePreviewDataFetched, + + onSendPressed: (message) { + unawaited(_handleSendPressed(message)); + }, + //showUserAvatars: false, + //showUserNames: true, + user: _localUser, + ), + ), + ), + ], ), ], ), - ], - ), - )); + ))); }); } } diff --git a/lib/chat_list/cubits/active_conversations_cubit.dart b/lib/chat_list/cubits/active_conversations_cubit.dart new file mode 100644 index 0000000..3df703f --- /dev/null +++ b/lib/chat_list/cubits/active_conversations_cubit.dart @@ -0,0 +1,24 @@ +import 'package:veilid_support/veilid_support.dart'; + +import '../../account_manager/account_manager.dart'; +import '../../contacts/contacts.dart'; +import '../../proto/proto.dart' as proto; +import '../../tools/tools.dart'; + +class ActiveConversationsCubit extends BlocMapCubit, ConversationCubit> { + ActiveConversationsCubit({required ActiveAccountInfo activeAccountInfo}) + : _activeAccountInfo = activeAccountInfo; + + Future addConversation({required proto.Contact contact}) async => + add(() => MapEntry( + contact.remoteConversationRecordKey, + ConversationCubit( + activeAccountInfo: _activeAccountInfo, + remoteIdentityPublicKey: contact.identityPublicKey, + localConversationRecordKey: contact.localConversationRecordKey, + remoteConversationRecordKey: contact.remoteConversationRecordKey, + ))); + + final ActiveAccountInfo _activeAccountInfo; +} diff --git a/lib/chat_list/cubits/cubits.dart b/lib/chat_list/cubits/cubits.dart index cafafff..1b77c00 100644 --- a/lib/chat_list/cubits/cubits.dart +++ b/lib/chat_list/cubits/cubits.dart @@ -1 +1,2 @@ +export 'active_conversations_cubit.dart'; export 'chat_list_cubit.dart'; diff --git a/lib/chat_list/views/chat_single_contact_item_widget.dart b/lib/chat_list/views/chat_single_contact_item_widget.dart index 746af91..37d1e33 100644 --- a/lib/chat_list/views/chat_single_contact_item_widget.dart +++ b/lib/chat_list/views/chat_single_contact_item_widget.dart @@ -24,6 +24,8 @@ class ChatSingleContactItemWidget extends StatelessWidget { final scale = theme.extension()!; final activeChatCubit = context.watch(); + final activeConversationsCubit = context.watch(); + final remoteConversationRecordKey = proto.TypedKeyProto.fromProto(_contact.remoteConversationRecordKey); final selected = activeChatCubit.state == remoteConversationRecordKey; @@ -67,6 +69,8 @@ class ChatSingleContactItemWidget extends StatelessWidget { // component is not dragged. child: ListTile( onTap: () { + xxx deal with async + activeConversationsCubit.addConversation(contact: _contact); activeChatCubit.setActiveChat(remoteConversationRecordKey); }, title: Text(_contact.editedProfile.name), diff --git a/lib/layout/home/home_account_ready/home_account_ready.dart b/lib/layout/home/home_account_ready/home_account_ready.dart index 6a42f97..f49f5db 100644 --- a/lib/layout/home/home_account_ready/home_account_ready.dart +++ b/lib/layout/home/home_account_ready/home_account_ready.dart @@ -9,6 +9,7 @@ import '../../../account_manager/account_manager.dart'; import '../../../chat/chat.dart'; import '../../../chat_list/chat_list.dart'; import '../../../contact_invitation/contact_invitation.dart'; +import '../../../contacts/contacts.dart'; import '../../../theme/theme.dart'; import '../../../tools/tools.dart'; import 'main_pager/main_pager.dart'; @@ -114,10 +115,17 @@ class HomeAccountReadyState extends State create: (context) => ContactInvitationListCubit( activeAccountInfo: activeAccountInfo, account: accountData.value)), + BlocProvider( + create: (context) => ContactListCubit( + activeAccountInfo: activeAccountInfo, + account: accountData.value)), BlocProvider( create: (context) => ChatListCubit( activeAccountInfo: activeAccountInfo, account: accountData.value)), + BlocProvider( + create: (context) => ActiveConversationsCubit( + activeAccountInfo: activeAccountInfo)), BlocProvider(create: (context) => ActiveChatCubit(null)) ], child: responsiveVisibility( diff --git a/lib/tools/bloc_map_cubit.dart b/lib/tools/bloc_map_cubit.dart new file mode 100644 index 0000000..1604807 --- /dev/null +++ b/lib/tools/bloc_map_cubit.dart @@ -0,0 +1,96 @@ +import 'dart:async'; + +import 'package:bloc/bloc.dart'; +import 'package:fast_immutable_collections/fast_immutable_collections.dart'; +import 'package:veilid_support/veilid_support.dart'; + +typedef BlocMapState = IMap; + +class _ItemEntry { + _ItemEntry({required this.bloc, required this.subscription}); + final B bloc; + final StreamSubscription subscription; +} + +abstract class BlocMapCubit> + extends Cubit> { + BlocMapCubit() + : _entries = {}, + _tagLock = AsyncTagLock(), + super(IMap()); + + @override + Future close() async { + await _entries.values.map((e) => e.subscription.cancel()).wait; + await _entries.values.map((e) => e.bloc.close()).wait; + await super.close(); + } + + Future add(MapEntry Function() create) { + // Create new element + final newElement = create(); + final key = newElement.key; + final bloc = newElement.value; + + return _tagLock.protect(key, closure: () async { + // Remove entry with the same key if it exists + await _internalRemove(key); + + // Add entry with this key + _entries[key] = _ItemEntry( + bloc: bloc, + subscription: bloc.stream.listen((data) { + // Add sub-cubit's state to the map state + emit(state.add(key, data)); + })); + + emit(state.add(key, bloc.state)); + }); + } + + Future _internalRemove(K key) async { + final sub = _entries.remove(key); + if (sub != null) { + await sub.subscription.cancel(); + await sub.bloc.close(); + } + } + + Future remove(K key) => _tagLock.protect(key, closure: () async { + await _internalRemove(key); + emit(state.remove(key)); + }); + + R operate(K key, {required R Function(B bloc) closure}) { + final bloc = _entries[key]!.bloc; + return closure(bloc); + } + + R? tryOperate(K key, {required R Function(B bloc) closure}) { + final entry = _entries[key]; + if (entry == null) { + return null; + } + return closure(entry.bloc); + } + + Future operateAsync(K key, + {required Future Function(B bloc) closure}) => + _tagLock.protect(key, closure: () async { + final bloc = _entries[key]!.bloc; + return closure(bloc); + }); + + Future tryOperateAsync(K key, + {required Future Function(B bloc) closure}) => + _tagLock.protect(key, closure: () async { + final entry = _entries[key]; + if (entry == null) { + return null; + } + return closure(entry.bloc); + }); + + final Map> _entries; + final AsyncTagLock _tagLock; +} diff --git a/lib/tools/stream_wrapper_cubit.dart b/lib/tools/stream_wrapper_cubit.dart index a858cb1..569304d 100644 --- a/lib/tools/stream_wrapper_cubit.dart +++ b/lib/tools/stream_wrapper_cubit.dart @@ -13,12 +13,12 @@ abstract class StreamWrapperCubit extends Cubit> { onError: (Object error, StackTrace stackTrace) { emit(AsyncValue.error(error, stackTrace)); }); + } - @override - Future close() async { - await _subscription.cancel(); - await super.close(); - } + @override + Future close() async { + await _subscription.cancel(); + await super.close(); } late final StreamSubscription _subscription; diff --git a/lib/tools/tools.dart b/lib/tools/tools.dart index 6750c62..fd18fd9 100644 --- a/lib/tools/tools.dart +++ b/lib/tools/tools.dart @@ -1,4 +1,5 @@ export 'animations.dart'; +export 'cubit_map.dart'; export 'enter_password.dart'; export 'enter_pin.dart'; export 'loggy.dart'; diff --git a/packages/veilid_support/lib/src/async_tag_lock.dart b/packages/veilid_support/lib/src/async_tag_lock.dart index 293b045..0187827 100644 --- a/packages/veilid_support/lib/src/async_tag_lock.dart +++ b/packages/veilid_support/lib/src/async_tag_lock.dart @@ -39,6 +39,15 @@ class AsyncTagLock { } } + Future protect(T tag, {required Future Function() closure}) async { + await lockTag(tag); + try { + return await closure(); + } finally { + unlockTag(tag); + } + } + // final Mutex _tableLock; final Map _locks; From 634543910b4919ebdf69c9c2d44494f98d9d7e46 Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Sun, 11 Feb 2024 00:29:58 -0500 Subject: [PATCH 31/68] messages work --- lib/chat/cubits/messages_cubit.dart | 1 + lib/chat/views/chat_component.dart | 167 +++--- .../active_conversation_messages_cubit.dart | 108 ++++ .../cubits/active_conversations_cubit.dart | 57 +- lib/chat_list/cubits/cubits.dart | 1 + .../chat_single_contact_item_widget.dart | 15 +- .../chat_single_contact_list_widget.dart | 2 - lib/contacts/cubits/conversation_cubit.dart | 1 + lib/tools/bloc_map_cubit.dart | 10 +- .../lib/src => lib/tools}/future_cubit.dart | 3 +- lib/tools/stream_wrapper_cubit.dart | 2 +- lib/tools/tools.dart | 4 +- lib/tools/transformer_cubit.dart | 21 + lib/tools/widget_helpers.dart | 2 +- .../views/signal_strength_meter.dart | 1 + packages/async_tools/.gitignore | 7 + packages/async_tools/analysis_options.yaml | 15 + .../example/async_tools_example.dart | 6 + packages/async_tools/lib/async_tools.dart | 6 + .../lib/src/async_tag_lock.dart | 24 +- .../lib/src/async_value.dart | 17 + .../lib/src/async_value.freezed.dart | 0 .../async_tools/lib/src/single_async.dart | 19 + packages/async_tools/pubspec.yaml | 18 + .../async_tools/test/async_tools_test.dart | 16 + packages/mutex/.gitignore | 16 + packages/mutex/CHANGELOG.md | 50 ++ packages/mutex/LICENSE | 24 + packages/mutex/README.md | 191 +++++++ packages/mutex/analysis_options.yaml | 15 + packages/mutex/example/example.dart | 114 ++++ packages/mutex/lib/mutex.dart | 11 + packages/mutex/lib/src/mutex.dart | 89 ++++ packages/mutex/lib/src/read_write_mutex.dart | 304 +++++++++++ packages/mutex/pubspec.yaml | 12 + .../mutex/test/mutex_multiple_read_test.dart | 102 ++++ packages/mutex/test/mutex_readwrite_test.dart | 486 ++++++++++++++++++ packages/mutex/test/mutex_test.dart | 341 ++++++++++++ .../lib/dht_support/src/dht_record_cubit.dart | 1 + .../lib/dht_support/src/dht_record_pool.dart | 1 + .../src/dht_short_array_cubit.dart | 1 + .../lib/src/async_table_db_backed_cubit.dart | 3 +- .../veilid_support/lib/veilid_support.dart | 3 - packages/veilid_support/pubspec.lock | 18 +- packages/veilid_support/pubspec.yaml | 3 +- pubspec.lock | 16 +- pubspec.yaml | 5 +- 47 files changed, 2206 insertions(+), 123 deletions(-) create mode 100644 lib/chat_list/cubits/active_conversation_messages_cubit.dart rename {packages/veilid_support/lib/src => lib/tools}/future_cubit.dart (90%) create mode 100644 lib/tools/transformer_cubit.dart create mode 100644 packages/async_tools/.gitignore create mode 100644 packages/async_tools/analysis_options.yaml create mode 100644 packages/async_tools/example/async_tools_example.dart create mode 100644 packages/async_tools/lib/async_tools.dart rename packages/{veilid_support => async_tools}/lib/src/async_tag_lock.dart (69%) rename packages/{veilid_support => async_tools}/lib/src/async_value.dart (90%) rename packages/{veilid_support => async_tools}/lib/src/async_value.freezed.dart (100%) create mode 100644 packages/async_tools/lib/src/single_async.dart create mode 100644 packages/async_tools/pubspec.yaml create mode 100644 packages/async_tools/test/async_tools_test.dart create mode 100644 packages/mutex/.gitignore create mode 100644 packages/mutex/CHANGELOG.md create mode 100644 packages/mutex/LICENSE create mode 100644 packages/mutex/README.md create mode 100644 packages/mutex/analysis_options.yaml create mode 100644 packages/mutex/example/example.dart create mode 100644 packages/mutex/lib/mutex.dart create mode 100644 packages/mutex/lib/src/mutex.dart create mode 100644 packages/mutex/lib/src/read_write_mutex.dart create mode 100644 packages/mutex/pubspec.yaml create mode 100644 packages/mutex/test/mutex_multiple_read_test.dart create mode 100644 packages/mutex/test/mutex_readwrite_test.dart create mode 100644 packages/mutex/test/mutex_test.dart diff --git a/lib/chat/cubits/messages_cubit.dart b/lib/chat/cubits/messages_cubit.dart index 76acfdc..b1846cf 100644 --- a/lib/chat/cubits/messages_cubit.dart +++ b/lib/chat/cubits/messages_cubit.dart @@ -1,5 +1,6 @@ import 'dart:async'; +import 'package:async_tools/async_tools.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:veilid_support/veilid_support.dart'; diff --git a/lib/chat/views/chat_component.dart b/lib/chat/views/chat_component.dart index 71cec22..9076a49 100644 --- a/lib/chat/views/chat_component.dart +++ b/lib/chat/views/chat_component.dart @@ -1,12 +1,17 @@ import 'dart:async'; +import 'package:async_tools/async_tools.dart'; import 'package:awesome_extensions/awesome_extensions.dart'; +import 'package:fast_immutable_collections/fast_immutable_collections.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_chat_types/flutter_chat_types.dart' as types; import 'package:flutter_chat_ui/flutter_chat_ui.dart'; +import 'package:veilid_support/veilid_support.dart'; import '../../account_manager/account_manager.dart'; +import '../../chat_list/chat_list.dart'; import '../../contacts/contacts.dart'; import '../../proto/proto.dart' as proto; import '../../theme/theme.dart'; @@ -14,10 +19,19 @@ import '../../tools/tools.dart'; import '../chat.dart'; class ChatComponent extends StatefulWidget { - const ChatComponent({super.key}); + const ChatComponent({required this.remoteConversationRecordKey, super.key}); @override ChatComponentState createState() => ChatComponentState(); + + final TypedKey remoteConversationRecordKey; + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.add(DiagnosticsProperty( + 'chatRemoteConversationKey', remoteConversationRecordKey)); + } } class ChatComponentState extends State { @@ -113,99 +127,92 @@ class ChatComponentState extends State { final textTheme = Theme.of(context).textTheme; final chatTheme = makeChatTheme(scale, textTheme); - final activeChatCubit = context.watch(); final contactListCubit = context.watch(); - final activeAccountInfo = context.watch(); - final activeChatContactKey = activeChatCubit.state; - if (activeChatContactKey == null) { - return const NoConversationWidget(); - } return contactListCubit.state.builder((context, contactList) { // Get active chat contact profile - final activeChatContactIdx = contactList.indexWhere( - (c) => activeChatContactKey == c.remoteConversationRecordKey); + final activeChatContactIdx = contactList.indexWhere((c) => + widget.remoteConversationRecordKey == c.remoteConversationRecordKey); late final proto.Contact activeChatContact; if (activeChatContactIdx == -1) { + // xxx: error, no contact for conversation... return const NoConversationWidget(); } else { activeChatContact = contactList[activeChatContactIdx]; } final contactName = activeChatContact.editedProfile.name; - // final protoMessages = - // ref.watch(activeConversationMessagesProvider).asData?.value; - // if (protoMessages == null) { - // return waitingPage(context); - // } - // final messages = []; - // for (final protoMessage in protoMessages) { - // final message = protoMessageToMessage(protoMessage); - // messages.insert(0, message); - // } + final messages = context.select>?>( + (x) => x.state[widget.remoteConversationRecordKey]); + if (messages == null) { + // xxx: error, no messages for conversation... + return const NoConversationWidget(); + } + return messages.builder((context, protoMessages) { + final messages = []; + for (final protoMessage in protoMessages) { + final message = protoMessageToMessage(protoMessage); + messages.insert(0, message); + } + return DefaultTextStyle( + style: textTheme.bodySmall!, + child: Align( + alignment: AlignmentDirectional.centerEnd, + child: Stack( + children: [ + Column( + children: [ + Container( + height: 48, + decoration: BoxDecoration( + color: scale.primaryScale.subtleBorder, + ), + child: Row(children: [ + Align( + alignment: AlignmentDirectional.centerStart, + child: Padding( + padding: const EdgeInsetsDirectional.fromSTEB( + 16, 0, 16, 0), + child: Text(contactName, + textAlign: TextAlign.start, + style: textTheme.titleMedium), + )), + const Spacer(), + IconButton( + icon: const Icon(Icons.close), + onPressed: () async { + context + .read() + .setActiveChat(null); + }).paddingLTRB(16, 0, 16, 0) + ]), + ), + Expanded( + child: DecoratedBox( + decoration: const BoxDecoration(), + child: Chat( + theme: chatTheme, + messages: messages, + //onAttachmentPressed: _handleAttachmentPressed, + //onMessageTap: _handleMessageTap, + //onPreviewDataFetched: _handlePreviewDataFetched, - return BlocProvider( - create: (context) => MessagesCubit( - activeAccountInfo: activeAccountInfo, - remoteIdentityPublicKey: activeChatContact.identityPublicKey, localConversationRecordKey: activeChatContact.localConversationRecordKey, localMessagesRecordKey: activeChatContact., + onSendPressed: (message) { + unawaited(_handleSendPressed(message)); + }, + //showUserAvatars: false, + //showUserNames: true, + user: _localUser, + ), + ), + ), + ], + ), + ], ), - child: DefaultTextStyle( - style: textTheme.bodySmall!, - child: Align( - alignment: AlignmentDirectional.centerEnd, - child: Stack( - children: [ - Column( - children: [ - Container( - height: 48, - decoration: BoxDecoration( - color: scale.primaryScale.subtleBorder, - ), - child: Row(children: [ - Align( - alignment: AlignmentDirectional.centerStart, - child: Padding( - padding: const EdgeInsetsDirectional.fromSTEB( - 16, 0, 16, 0), - child: Text(contactName, - textAlign: TextAlign.start, - style: textTheme.titleMedium), - )), - const Spacer(), - IconButton( - icon: const Icon(Icons.close), - onPressed: () async { - context - .read() - .setActiveChat(null); - }).paddingLTRB(16, 0, 16, 0) - ]), - ), - Expanded( - child: DecoratedBox( - decoration: const BoxDecoration(), - child: Chat( - theme: chatTheme, - messages: messages, - //onAttachmentPressed: _handleAttachmentPressed, - //onMessageTap: _handleMessageTap, - //onPreviewDataFetched: _handlePreviewDataFetched, - - onSendPressed: (message) { - unawaited(_handleSendPressed(message)); - }, - //showUserAvatars: false, - //showUserNames: true, - user: _localUser, - ), - ), - ), - ], - ), - ], - ), - ))); + )); + }); }); } } diff --git a/lib/chat_list/cubits/active_conversation_messages_cubit.dart b/lib/chat_list/cubits/active_conversation_messages_cubit.dart new file mode 100644 index 0000000..d7db7a5 --- /dev/null +++ b/lib/chat_list/cubits/active_conversation_messages_cubit.dart @@ -0,0 +1,108 @@ +import 'dart:async'; + +import 'package:async_tools/async_tools.dart'; +import 'package:fast_immutable_collections/fast_immutable_collections.dart'; +import 'package:veilid_support/veilid_support.dart'; + +import '../../account_manager/account_manager.dart'; +import '../../chat/chat.dart'; +import '../../proto/proto.dart' as proto; +import '../../tools/tools.dart'; +import 'active_conversations_cubit.dart'; + +class ActiveConversationMessagesCubit extends BlocMapCubit>, MessagesCubit> { + ActiveConversationMessagesCubit({ + required ActiveAccountInfo activeAccountInfo, + required Stream stream, + }) : _activeAccountInfo = activeAccountInfo { + // + _subscription = stream.listen(updateMessageCubits); + } + + @override + Future close() async { + await _subscription.cancel(); + await super.close(); + } + + // Determine which conversations have been added, deleted, or changed + // and update this cubit's state appropriately + void updateMessageCubits(ActiveConversationsBlocMapState newInputState) { + // Use a singlefuture here to ensure we get dont lose any updates + // If the ActiveConversations stream gives us an update while we are + // still processing the last update, the most recent input state will + // be saved and processed eventually. + singleFuture(this, () async { + var newActiveConversationsState = newInputState; + var done = false; + while (!done) { + // Build lists of changes to conversations + final deleted = _lastActiveConversationsState.keys + .where((k) => !newActiveConversationsState.containsKey(k)); + final added = newActiveConversationsState.keys + .where((k) => !_lastActiveConversationsState.containsKey(k)); + final changed = _lastActiveConversationsState.where((k, v) { + final nv = newActiveConversationsState[k]; + if (nv == null) { + return false; + } + return nv != v; + }).keys; + + // Process all deleted conversations + for (final d in deleted) { + await remove(d); + } + + // Process all added and changed conversations + for (final a in [...added, ...changed]) { + final av = newActiveConversationsState[a]!; + await av.when( + data: (state) => _addConversationMessages( + contact: state.contact, + localConversation: state.localConversation, + remoteConversation: state.remoteConversation), + loading: () => addState(a, const AsyncValue.loading()), + error: (error, stackTrace) => + addState(a, AsyncValue.error(error, stackTrace))); + } + + // Keep this state for the next time + _lastActiveConversationsState = newActiveConversationsState; + + // See if there's another state change to process + final next = _nextActiveConversationsState; + _nextActiveConversationsState = null; + if (next != null) { + newActiveConversationsState = next; + } else { + done = true; + } + } + }, onBusy: () { + // Keep this state until we process again + _nextActiveConversationsState = newInputState; + }); + } + + Future _addConversationMessages( + {required proto.Contact contact, + required proto.Conversation localConversation, + required proto.Conversation remoteConversation}) async => + add(() => MapEntry( + contact.remoteConversationRecordKey, + MessagesCubit( + activeAccountInfo: _activeAccountInfo, + remoteIdentityPublicKey: contact.identityPublicKey, + localConversationRecordKey: contact.localConversationRecordKey, + remoteConversationRecordKey: contact.remoteConversationRecordKey, + localMessagesRecordKey: localConversation.messages, + remoteMessagesRecordKey: remoteConversation.messages))); + + final ActiveAccountInfo _activeAccountInfo; + ActiveConversationsBlocMapState _lastActiveConversationsState = + ActiveConversationsBlocMapState(); + ActiveConversationsBlocMapState? _nextActiveConversationsState; + late final StreamSubscription _subscription; +} diff --git a/lib/chat_list/cubits/active_conversations_cubit.dart b/lib/chat_list/cubits/active_conversations_cubit.dart index 3df703f..0045f42 100644 --- a/lib/chat_list/cubits/active_conversations_cubit.dart +++ b/lib/chat_list/cubits/active_conversations_cubit.dart @@ -1,3 +1,6 @@ +import 'package:async_tools/async_tools.dart'; +import 'package:equatable/equatable.dart'; +import 'package:meta/meta.dart'; import 'package:veilid_support/veilid_support.dart'; import '../../account_manager/account_manager.dart'; @@ -5,20 +8,60 @@ import '../../contacts/contacts.dart'; import '../../proto/proto.dart' as proto; import '../../tools/tools.dart'; +@immutable +class ActiveConversationState extends Equatable { + const ActiveConversationState({ + required this.contact, + required this.localConversation, + required this.remoteConversation, + }); + + final proto.Contact contact; + final proto.Conversation localConversation; + final proto.Conversation remoteConversation; + + @override + List get props => [contact, localConversation, remoteConversation]; +} + +typedef ActiveConversationCubit = TransformerCubit< + AsyncValue, AsyncValue>; + +typedef ActiveConversationsBlocMapState + = BlocMapState>; + +// Map of remoteConversationRecordKey to ActiveConversationCubit +// Wraps a conversation cubit to only expose completely built conversations class ActiveConversationsCubit extends BlocMapCubit, ConversationCubit> { + AsyncValue, ActiveConversationCubit> { ActiveConversationsCubit({required ActiveAccountInfo activeAccountInfo}) : _activeAccountInfo = activeAccountInfo; + // Add an active conversation to be tracked for changes Future addConversation({required proto.Contact contact}) async => add(() => MapEntry( contact.remoteConversationRecordKey, - ConversationCubit( - activeAccountInfo: _activeAccountInfo, - remoteIdentityPublicKey: contact.identityPublicKey, - localConversationRecordKey: contact.localConversationRecordKey, - remoteConversationRecordKey: contact.remoteConversationRecordKey, - ))); + TransformerCubit( + ConversationCubit( + activeAccountInfo: _activeAccountInfo, + remoteIdentityPublicKey: contact.identityPublicKey, + localConversationRecordKey: contact.localConversationRecordKey, + remoteConversationRecordKey: + contact.remoteConversationRecordKey, + ), + // Transformer that only passes through completed conversations + // along with the contact that corresponds to the completed + // conversation + transform: (avstate) => avstate.when( + data: (data) => (data.localConversation == null || + data.remoteConversation == null) + ? const AsyncValue.loading() + : AsyncValue.data(ActiveConversationState( + contact: contact, + localConversation: data.localConversation!, + remoteConversation: data.remoteConversation!)), + loading: AsyncValue.loading, + error: AsyncValue.error)))); final ActiveAccountInfo _activeAccountInfo; } diff --git a/lib/chat_list/cubits/cubits.dart b/lib/chat_list/cubits/cubits.dart index 1b77c00..474f5cb 100644 --- a/lib/chat_list/cubits/cubits.dart +++ b/lib/chat_list/cubits/cubits.dart @@ -1,2 +1,3 @@ +export 'active_conversation_messages_cubit.dart'; export 'active_conversations_cubit.dart'; export 'chat_list_cubit.dart'; diff --git a/lib/chat_list/views/chat_single_contact_item_widget.dart b/lib/chat_list/views/chat_single_contact_item_widget.dart index 37d1e33..8a3fda7 100644 --- a/lib/chat_list/views/chat_single_contact_item_widget.dart +++ b/lib/chat_list/views/chat_single_contact_item_widget.dart @@ -1,3 +1,4 @@ +import 'package:async_tools/async_tools.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -24,7 +25,9 @@ class ChatSingleContactItemWidget extends StatelessWidget { final scale = theme.extension()!; final activeChatCubit = context.watch(); - final activeConversationsCubit = context.watch(); + // final activeConversation = context.select(); + // final activeConversationMessagesCubit = + // context.watch(); xxx does this need to be here? final remoteConversationRecordKey = proto.TypedKeyProto.fromProto(_contact.remoteConversationRecordKey); @@ -69,9 +72,13 @@ class ChatSingleContactItemWidget extends StatelessWidget { // component is not dragged. child: ListTile( onTap: () { - xxx deal with async - activeConversationsCubit.addConversation(contact: _contact); - activeChatCubit.setActiveChat(remoteConversationRecordKey); + final activeConversationsCubit = + context.read(); + singleFuture(activeChatCubit, () async { + await activeConversationsCubit.addConversation( + contact: _contact); + activeChatCubit.setActiveChat(remoteConversationRecordKey); + }); }, title: Text(_contact.editedProfile.name), diff --git a/lib/chat_list/views/chat_single_contact_list_widget.dart b/lib/chat_list/views/chat_single_contact_list_widget.dart index 3d3f3eb..4a31e2d 100644 --- a/lib/chat_list/views/chat_single_contact_list_widget.dart +++ b/lib/chat_list/views/chat_single_contact_list_widget.dart @@ -10,8 +10,6 @@ import '../../proto/proto.dart' as proto; import '../../theme/theme.dart'; import '../../tools/tools.dart'; import '../chat_list.dart'; -import 'chat_single_contact_item_widget.dart'; -import 'empty_chat_list_widget.dart'; class ChatSingleContactListWidget extends StatelessWidget { const ChatSingleContactListWidget({super.key}); diff --git a/lib/contacts/cubits/conversation_cubit.dart b/lib/contacts/cubits/conversation_cubit.dart index 9590a08..c927a1b 100644 --- a/lib/contacts/cubits/conversation_cubit.dart +++ b/lib/contacts/cubits/conversation_cubit.dart @@ -5,6 +5,7 @@ import 'dart:async'; import 'dart:convert'; +import 'package:async_tools/async_tools.dart'; import 'package:equatable/equatable.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:meta/meta.dart'; diff --git a/lib/tools/bloc_map_cubit.dart b/lib/tools/bloc_map_cubit.dart index 1604807..04d7693 100644 --- a/lib/tools/bloc_map_cubit.dart +++ b/lib/tools/bloc_map_cubit.dart @@ -1,8 +1,8 @@ import 'dart:async'; +import 'package:async_tools/async_tools.dart'; import 'package:bloc/bloc.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; -import 'package:veilid_support/veilid_support.dart'; typedef BlocMapState = IMap; @@ -48,6 +48,14 @@ abstract class BlocMapCubit> }); } + Future addState(K key, S value) => + _tagLock.protect(key, closure: () async { + // Remove entry with the same key if it exists + await _internalRemove(key); + + emit(state.add(key, value)); + }); + Future _internalRemove(K key) async { final sub = _entries.remove(key); if (sub != null) { diff --git a/packages/veilid_support/lib/src/future_cubit.dart b/lib/tools/future_cubit.dart similarity index 90% rename from packages/veilid_support/lib/src/future_cubit.dart rename to lib/tools/future_cubit.dart index 851c422..77d8387 100644 --- a/packages/veilid_support/lib/src/future_cubit.dart +++ b/lib/tools/future_cubit.dart @@ -1,9 +1,8 @@ import 'dart:async'; +import 'package:async_tools/async_tools.dart'; import 'package:bloc/bloc.dart'; -import '../veilid_support.dart'; - abstract class FutureCubit extends Cubit> { FutureCubit(Future fut) : super(const AsyncValue.loading()) { unawaited(fut.then((value) { diff --git a/lib/tools/stream_wrapper_cubit.dart b/lib/tools/stream_wrapper_cubit.dart index 569304d..732695b 100644 --- a/lib/tools/stream_wrapper_cubit.dart +++ b/lib/tools/stream_wrapper_cubit.dart @@ -1,7 +1,7 @@ import 'dart:async'; +import 'package:async_tools/async_tools.dart'; import 'package:bloc/bloc.dart'; -import 'package:veilid_support/veilid_support.dart'; abstract class StreamWrapperCubit extends Cubit> { StreamWrapperCubit(Stream stream, {State? defaultState}) diff --git a/lib/tools/tools.dart b/lib/tools/tools.dart index fd18fd9..35792b8 100644 --- a/lib/tools/tools.dart +++ b/lib/tools/tools.dart @@ -1,7 +1,8 @@ export 'animations.dart'; -export 'cubit_map.dart'; +export 'bloc_map_cubit.dart'; export 'enter_password.dart'; export 'enter_pin.dart'; +export 'future_cubit.dart'; export 'loggy.dart'; export 'phono_byte.dart'; export 'responsive.dart'; @@ -10,5 +11,6 @@ export 'shared_preferences.dart'; export 'state_logger.dart'; export 'stream_listenable.dart'; export 'stream_wrapper_cubit.dart'; +export 'transformer_cubit.dart'; export 'widget_helpers.dart'; export 'window_control.dart'; diff --git a/lib/tools/transformer_cubit.dart b/lib/tools/transformer_cubit.dart new file mode 100644 index 0000000..e9aa9b6 --- /dev/null +++ b/lib/tools/transformer_cubit.dart @@ -0,0 +1,21 @@ +import 'dart:async'; + +import 'package:bloc/bloc.dart'; + +class TransformerCubit extends Cubit { + TransformerCubit(this.input, {required this.transform}) + : super(transform(input.state)) { + _subscription = input.stream.listen((event) => emit(transform(event))); + } + + @override + Future close() async { + await _subscription.cancel(); + await input.close(); + await super.close(); + } + + Cubit input; + T Function(S) transform; + late final StreamSubscription _subscription; +} diff --git a/lib/tools/widget_helpers.dart b/lib/tools/widget_helpers.dart index 7812c5e..9eddc83 100644 --- a/lib/tools/widget_helpers.dart +++ b/lib/tools/widget_helpers.dart @@ -1,3 +1,4 @@ +import 'package:async_tools/async_tools.dart'; import 'package:awesome_extensions/awesome_extensions.dart'; import 'package:blurry_modal_progress_hud/blurry_modal_progress_hud.dart'; import 'package:flutter/material.dart'; @@ -6,7 +7,6 @@ import 'package:flutter_spinkit/flutter_spinkit.dart'; import 'package:flutter_translate/flutter_translate.dart'; import 'package:motion_toast/motion_toast.dart'; import 'package:quickalert/quickalert.dart'; -import 'package:veilid_support/veilid_support.dart'; import '../theme/theme.dart'; diff --git a/lib/veilid_processor/views/signal_strength_meter.dart b/lib/veilid_processor/views/signal_strength_meter.dart index 9b189f4..1b94f78 100644 --- a/lib/veilid_processor/views/signal_strength_meter.dart +++ b/lib/veilid_processor/views/signal_strength_meter.dart @@ -1,3 +1,4 @@ +import 'package:async_tools/async_tools.dart'; import 'package:awesome_extensions/awesome_extensions.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; diff --git a/packages/async_tools/.gitignore b/packages/async_tools/.gitignore new file mode 100644 index 0000000..3cceda5 --- /dev/null +++ b/packages/async_tools/.gitignore @@ -0,0 +1,7 @@ +# https://dart.dev/guides/libraries/private-files +# Created by `dart pub` +.dart_tool/ + +# Avoid committing pubspec.lock for library packages; see +# https://dart.dev/guides/libraries/private-files#pubspeclock. +pubspec.lock diff --git a/packages/async_tools/analysis_options.yaml b/packages/async_tools/analysis_options.yaml new file mode 100644 index 0000000..e1620f7 --- /dev/null +++ b/packages/async_tools/analysis_options.yaml @@ -0,0 +1,15 @@ +include: package:lint_hard/all.yaml +analyzer: + errors: + invalid_annotation_target: ignore + exclude: + - '**/*.g.dart' + - '**/*.freezed.dart' + - '**/*.pb.dart' + - '**/*.pbenum.dart' + - '**/*.pbjson.dart' + - '**/*.pbserver.dart' +linter: + rules: + unawaited_futures: true + avoid_positional_boolean_parameters: false \ No newline at end of file diff --git a/packages/async_tools/example/async_tools_example.dart b/packages/async_tools/example/async_tools_example.dart new file mode 100644 index 0000000..33a41ab --- /dev/null +++ b/packages/async_tools/example/async_tools_example.dart @@ -0,0 +1,6 @@ +// import 'package:async_tools/async_tools.dart'; + +// void main() { +// var awesome = Awesome(); +// print('awesome: ${awesome.isAwesome}'); +// } diff --git a/packages/async_tools/lib/async_tools.dart b/packages/async_tools/lib/async_tools.dart new file mode 100644 index 0000000..4dbe72e --- /dev/null +++ b/packages/async_tools/lib/async_tools.dart @@ -0,0 +1,6 @@ +/// Async Tools +library; + +export 'src/async_tag_lock.dart'; +export 'src/async_value.dart'; +export 'src/single_async.dart'; diff --git a/packages/veilid_support/lib/src/async_tag_lock.dart b/packages/async_tools/lib/src/async_tag_lock.dart similarity index 69% rename from packages/veilid_support/lib/src/async_tag_lock.dart rename to packages/async_tools/lib/src/async_tag_lock.dart index 0187827..a0f6117 100644 --- a/packages/veilid_support/lib/src/async_tag_lock.dart +++ b/packages/async_tools/lib/src/async_tag_lock.dart @@ -2,8 +2,8 @@ import 'package:mutex/mutex.dart'; class _AsyncTagLockEntry { _AsyncTagLockEntry() - : mutex = Mutex(), - waitingCount = 1; + : mutex = Mutex.locked(), + waitingCount = 0; // Mutex mutex; int waitingCount; @@ -16,18 +16,28 @@ class AsyncTagLock { Future lockTag(T tag) async { await _tableLock.protect(() async { - var lockEntry = _locks[tag]; + final lockEntry = _locks[tag]; if (lockEntry != null) { lockEntry.waitingCount++; + await lockEntry.mutex.acquire(); + lockEntry.waitingCount--; } else { - lockEntry = _locks[tag] = _AsyncTagLockEntry(); + _locks[tag] = _AsyncTagLockEntry(); } - - await lockEntry.mutex.acquire(); - lockEntry.waitingCount--; }); } + bool isLocked(T tag) => _locks.containsKey(tag); + + bool tryLock(T tag) { + final lockEntry = _locks[tag]; + if (lockEntry != null) { + return false; + } + _locks[tag] = _AsyncTagLockEntry(); + return true; + } + void unlockTag(T tag) { final lockEntry = _locks[tag]!; if (lockEntry.waitingCount == 0) { diff --git a/packages/veilid_support/lib/src/async_value.dart b/packages/async_tools/lib/src/async_value.dart similarity index 90% rename from packages/veilid_support/lib/src/async_value.dart rename to packages/async_tools/lib/src/async_value.dart index 8f02478..aee070d 100644 --- a/packages/veilid_support/lib/src/async_value.dart +++ b/packages/async_tools/lib/src/async_value.dart @@ -169,4 +169,21 @@ abstract class AsyncValue with _$AsyncValue { loading: () => const AsyncValue.loading(), error: AsyncValue.error, ); + + /// Check two AsyncData instances for equality + bool equalsData(AsyncValue other, + {required bool Function(T a, T b) equals}) => + other.when( + data: (nd) => when( + data: (d) => equals(d, nd), + loading: () => true, + error: (_e, _st) => true), + loading: () => when( + data: (_) => true, + loading: () => false, + error: (_e, _st) => true), + error: (ne, nst) => when( + data: (_) => true, + loading: () => true, + error: (e, st) => e != ne || st != nst)); } diff --git a/packages/veilid_support/lib/src/async_value.freezed.dart b/packages/async_tools/lib/src/async_value.freezed.dart similarity index 100% rename from packages/veilid_support/lib/src/async_value.freezed.dart rename to packages/async_tools/lib/src/async_value.freezed.dart diff --git a/packages/async_tools/lib/src/single_async.dart b/packages/async_tools/lib/src/single_async.dart new file mode 100644 index 0000000..aee9bc2 --- /dev/null +++ b/packages/async_tools/lib/src/single_async.dart @@ -0,0 +1,19 @@ +import 'dart:async'; + +import 'async_tag_lock.dart'; + +AsyncTagLock _keys = AsyncTagLock(); + +void singleFuture(Object tag, Future Function() closure, + {void Function()? onBusy}) { + if (!_keys.tryLock(tag)) { + if (onBusy != null) { + onBusy(); + } + return; + } + unawaited(() async { + await closure(); + _keys.unlockTag(tag); + }()); +} diff --git a/packages/async_tools/pubspec.yaml b/packages/async_tools/pubspec.yaml new file mode 100644 index 0000000..a495170 --- /dev/null +++ b/packages/async_tools/pubspec.yaml @@ -0,0 +1,18 @@ +name: async_tools +description: Useful data structures and tools for async/Future code +version: 1.0.0 +publish_to: none + +environment: + sdk: ^3.2.6 + +# Add regular dependencies here. +dependencies: + freezed_annotation: ^2.2.0 + mutex: + path: ../mutex + +dev_dependencies: + freezed: ^2.3.5 + lint_hard: ^4.0.0 + test: ^1.24.0 diff --git a/packages/async_tools/test/async_tools_test.dart b/packages/async_tools/test/async_tools_test.dart new file mode 100644 index 0000000..0d54797 --- /dev/null +++ b/packages/async_tools/test/async_tools_test.dart @@ -0,0 +1,16 @@ +// import 'package:async_tools/async_tools.dart'; +// import 'package:test/test.dart'; + +// void main() { +// group('A group of tests', () { +// final awesome = Awesome(); + +// setUp(() { +// // Additional setup goes here. +// }); + +// test('First Test', () { +// expect(awesome.isAwesome, isTrue); +// }); +// }); +// } diff --git a/packages/mutex/.gitignore b/packages/mutex/.gitignore new file mode 100644 index 0000000..2ca4cae --- /dev/null +++ b/packages/mutex/.gitignore @@ -0,0 +1,16 @@ +# Files and directories created by pub +.packages +.pub/ +.dart_tool/ +build/ +packages +pubspec.lock + +# Directory created by dartdoc +doc/api/ + +# JetBrains IDEs +.idea/ +*.iml +*.ipr +*.iws diff --git a/packages/mutex/CHANGELOG.md b/packages/mutex/CHANGELOG.md new file mode 100644 index 0000000..9115310 --- /dev/null +++ b/packages/mutex/CHANGELOG.md @@ -0,0 +1,50 @@ +## 3.1.0 + +- Increased minimum Dart SDK to 2.15.0 for `unawaited` function. +- Added development dependencies lints ^2.1.1 and pana: ^0.21.37. +- Fixed code to remove lint warnings. + +## 3.0.1 + +- Fixed bug with new read mutexes preventing a write mutex from being acquired. + +## 3.0.0 + +- BREAKING CHANGE: critical section functions must return a Future. + - This is unlikely to affect real-world code, since only functions + containing asynchronous code would be critical. +- Protect method returns Future to the value from the critical section. + +## 2.0.0 + +- Null safety release. + +## 2.0.0-nullsafety.0 + +- Pre-release version: updated library to null safety (Non-nullable by default). +- Removed support for Dart 1.x. + +## 1.1.0 + +- Added protect, protectRead and protectWrite convenience methods. +- Improved tests to not depend on timing. + +## 1.0.3 + +- Added an example. + +## 1.0.2 + +- Code clean up to satisfy pana 0.13.2 health checks. + +## 1.0.1 + +- Fixed dartanalyzer warnings. + +## 1.0.0 + +- Updated the upper bound of the SDK constraint to <3.0.0. + +## 0.0.1 + +- Initial version diff --git a/packages/mutex/LICENSE b/packages/mutex/LICENSE new file mode 100644 index 0000000..eb40cc8 --- /dev/null +++ b/packages/mutex/LICENSE @@ -0,0 +1,24 @@ +Copyright (c) 2016, Hoylen Sue. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * Neither the name of the nor the + names of its contributors may be used to endorse or promote products + derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY +DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/packages/mutex/README.md b/packages/mutex/README.md new file mode 100644 index 0000000..1df6b41 --- /dev/null +++ b/packages/mutex/README.md @@ -0,0 +1,191 @@ +# mutex + +A library for creating locks to ensure mutual exclusion when +running critical sections of code. + +## Purpose + +Mutexes can be used to protect critical sections of code to prevent +race conditions. + +Although Dart uses a single thread of execution, race conditions +can still occur when asynchronous operations are used inside +critical sections. For example, + + x = 42; + synchronousOperations(); // this does not modify x + assert(x == 42); // x will NOT have changed + + y = 42; // a variable that other asynchronous code can modify + await asynchronousOperations(); // this does NOT modify y, but... + // There is NO GUARANTEE other async code didn't run and change it! + assert(y == 42 || y != 42); // WARNING: y might have changed + +An example is when Dart is used to implement a server-side Web server +that updates a database (assuming database transactions are not being +used). The update involves querying the database, performing +calculations on those retrieved values, and then updating the database +with the result. You don't want the database to be changed by +"something else" while performing the calculations, since the results +you would write will not incorporate those other changes. That +"something else" could be the same Web server handling another request +in parallel. + +This package provides a normal mutex and a read-write mutex. + +## Mutex + +A mutex guarantees at most only one lock can exist at any one time. + +If the lock has already been acquired, attempts to acquire another +lock will be blocked until the lock has been released. + +```dart +import 'package:mutex/mutex.dart'; + +... + +final m = Mutex(); +``` + +Acquiring the lock before running the critical section of code, +and then releasing the lock. + +```dart +await m.acquire(); +// No other lock can be acquired until the lock is released + +try { + // critical section with asynchronous code + await ... +} finally { + m.release(); +} +``` + +### protect + +The following code uses the _protect_ convenience method to do the +same thing as the above code. Use the convenence method whenever +possible, since it ensures the lock will always be released. + +```dart +await m.protect(() async { + // critical section +}); +``` + +If the critial section returns a Future to a value, the _protect_ +convenience method will return a Future to that value. + +```dart +final result = await m.protect(() async { + // critical section + return valueFromCriticalSection; +}); +// result contains the valueFromCriticalSection +``` + + +## Read-write mutex + +A read-write mutex allows multiple _reads locks_ to be exist +simultaneously, but at most only one _write lock_ can exist at any one +time. A _write lock_ and any _read locks_ cannot both exist together +at the same time. + +If there is one or more _read locks_, attempts to acquire a _write +lock_ will be blocked until all the _read locks_ have been +released. But attempts to acquire more _read locks_ will not be +blocked. If there is a _write lock_, attempts to acquire any lock +(read or write) will be blocked until that _write lock_ is released. + +A read-write mutex can also be described as a single-writer mutex, +multiple-reader mutex, or a reentrant lock. + +```dart +import 'package:mutex/mutex.dart'; + +... + +final m = ReadWriteMutex(); +``` + +Acquiring a write lock: + + await m.acquireWrite(); + // No other locks (read or write) can be acquired until released + + try { + // critical write section with asynchronous code + await ... + } finally { + m.release(); + } + +Acquiring a read lock: + + await m.acquireRead(); + // No write lock can be acquired until all read locks are released, + // but additional read locks can be acquired. + + try { + // critical read section with asynchronous code + await ... + } finally { + m.release(); + } + +### protectWrite and protectRead + +The following code uses the _protectWrite_ and _protectRead_ +convenience methods to do the same thing as the above code. Use the +convenence method whenever possible, since it ensures the lock will +always be released. + +```dart +await m.protectWrite(() async { + // critical write section +}); + +await m.protectRead(() async { + // critical read section +}); +``` + +If the critial section returns a Future to a value, these convenience +methods will return a Future to that value. + +```dart +final result1 await m.protectWrite(() async { + // critical write section + return valueFromCritialSection1; +}); +// result1 contains the valueFromCriticalSection1 + +final result2 = await m.protectRead(() async { + // critical read section + return valueFromCritialSection2; +}); +// result2 contains the valueFromCriticalSection2 +``` + +## When mutual exclusion is not needed + +The critical section should always contain some asynchronous code. If +the critical section only contains synchronous code, there is no need +to put it in a critical section. In Dart, synchronous code cannot be +interrupted, so there is no need to protect it using mutual exclusion. + +Also, if the critical section does not involve data or shared +resources that can be accessed by other asynchronous code, it also +does not need to be protected. For example, if it only uses local +variables that other asynchronous code won't have access to: while the +other asynchronous code could run, it won't be able to make unexpected +changes to the local variables it can't access. + +## Features and bugs + +Please file feature requests and bugs at the [issue tracker][tracker]. + +[tracker]: https://github.com/hoylen/dart-mutex/issues diff --git a/packages/mutex/analysis_options.yaml b/packages/mutex/analysis_options.yaml new file mode 100644 index 0000000..e1620f7 --- /dev/null +++ b/packages/mutex/analysis_options.yaml @@ -0,0 +1,15 @@ +include: package:lint_hard/all.yaml +analyzer: + errors: + invalid_annotation_target: ignore + exclude: + - '**/*.g.dart' + - '**/*.freezed.dart' + - '**/*.pb.dart' + - '**/*.pbenum.dart' + - '**/*.pbjson.dart' + - '**/*.pbserver.dart' +linter: + rules: + unawaited_futures: true + avoid_positional_boolean_parameters: false \ No newline at end of file diff --git a/packages/mutex/example/example.dart b/packages/mutex/example/example.dart new file mode 100644 index 0000000..c13b007 --- /dev/null +++ b/packages/mutex/example/example.dart @@ -0,0 +1,114 @@ +// Mutex example. +// +// This example demonstrates why a mutex is needed. + +import 'dart:async'; +import 'dart:math'; +import 'package:mutex/mutex.dart'; + +//---------------------------------------------------------------- +// Random asynchronous delays to try and simulate race conditions. + +const _maxDelay = 500; // milliseconds + +final _random = Random(); + +Future randomDelay() async { + await Future.delayed( + Duration(milliseconds: _random.nextInt(_maxDelay))); +} + +//---------------------------------------------------------------- +/// Account balance. +/// +/// The classical example of a race condition is when a bank account is updated +/// by different simultaneous operations. + +int balance = 0; + +//---------------------------------------------------------------- +/// Deposit without using mutex. + +Future unsafeUpdate(int id, int depositAmount) async { + // Random delay before updating starts + await randomDelay(); + + // Add the deposit to the balance. But this operation is not atomic if + // there are asynchronous operations in it (as simulated by the randomDelay). + + final oldBalance = balance; + await randomDelay(); + balance = oldBalance + depositAmount; + + print(' [$id] added $depositAmount to $oldBalance -> $balance'); +} + +//---------------------------------------------------------------- +/// Deposit using mutex. + +Mutex m = Mutex(); + +Future safeUpdate(int id, int depositAmount) async { + // Random delay before updating starts + await randomDelay(); + + // Acquire the mutex before running the critical section of code + + await m.protect(() async { + // critical section + + // This is the same as the unsafe update. But since it is performed only + // when the mutex is acquired, it is safe: no other safe update can happen + // until this mutex is released. + + final oldBalance = balance; + await randomDelay(); + balance = oldBalance + depositAmount; + + // end of critical section + + print(' [$id] added $depositAmount to $oldBalance -> $balance'); + }); +} + +//---------------------------------------------------------------- +/// Make a series of deposits and see if the final balance is correct. + +Future makeDeposits({bool safe = true}) async { + print(safe ? 'Using mutex:' : 'Not using mutex:'); + + const numberDeposits = 10; + const amount = 10; + + balance = 0; + + // Create a set of operations, each attempting to deposit the same amount + // into the account. + + final operations = >[]; + for (var x = 0; x < numberDeposits; x++) { + final f = (safe) ? safeUpdate(x, amount) : unsafeUpdate(x, amount); + operations.add(f); + } + + // Wait for all the deposit operations to finish + + await Future.wait(operations); + + // Check if all of the operations succeeded + + final expected = numberDeposits * amount; + if (balance != expected) { + print('Error: deposits were lost (final balance $balance != $expected)'); + } else { + print('Success: no deposits were lost'); + } +} + +//---------------------------------------------------------------- + +void main() async { + await makeDeposits(safe: false); + print(''); + await makeDeposits(safe: true); +} diff --git a/packages/mutex/lib/mutex.dart b/packages/mutex/lib/mutex.dart new file mode 100644 index 0000000..ba224d1 --- /dev/null +++ b/packages/mutex/lib/mutex.dart @@ -0,0 +1,11 @@ +// Copyright (c) 2016, Hoylen Sue. All rights reserved. Use of this source code +// is governed by a BSD-style license that can be found in the LICENSE file. + +/// Mutual exclusion. +/// +library mutex; + +import 'dart:async'; + +part 'src/mutex.dart'; +part 'src/read_write_mutex.dart'; diff --git a/packages/mutex/lib/src/mutex.dart b/packages/mutex/lib/src/mutex.dart new file mode 100644 index 0000000..1c9e9ec --- /dev/null +++ b/packages/mutex/lib/src/mutex.dart @@ -0,0 +1,89 @@ +part of mutex; + +/// Mutual exclusion. +/// +/// The [protect] method is a convenience method for acquiring a lock before +/// running critical code, and then releasing the lock afterwards. Using this +/// convenience method will ensure the lock is always released after use. +/// +/// Usage: +/// +/// m = Mutex(); +/// +/// await m.protect(() async { +/// // critical section +/// }); +/// +/// Alternatively, a lock can be explicitly acquired and managed. In this +/// situation, the program is responsible for releasing the lock after it +/// have been used. Failure to release the lock will prevent other code for +/// ever acquiring the lock. +/// +/// m = Mutex(); +/// +/// await m.acquire(); +/// try { +/// // critical section +/// } +/// finally { +/// m.release(); +/// } + +class Mutex { + //================================================================ + // Constructors + Mutex() : _rwMutex = ReadWriteMutex(); + Mutex.locked() : _rwMutex = ReadWriteMutex.writeLocked(); + + // Implemented as a ReadWriteMutex that is used only with write locks. + final ReadWriteMutex _rwMutex; + + /// Indicates if a lock has been acquired and not released. + bool get isLocked => _rwMutex.isLocked; + + /// Acquire a lock + /// + /// Returns a future that will be completed when the lock has been acquired. + /// + /// Consider using the convenience method [protect], otherwise the caller + /// is responsible for making sure the lock is released after it is no longer + /// needed. Failure to release the lock means no other code can acquire the + /// lock. + + Future acquire() => _rwMutex.acquireWrite(); + + /// Release a lock. + /// + /// Release a lock that has been acquired. + + void release() => _rwMutex.release(); + + /// Convenience method for protecting a function with a lock. + /// + /// This method guarantees a lock is always acquired before invoking the + /// [criticalSection] function. It also guarantees the lock is always + /// released. + /// + /// A critical section should always contain asynchronous code, since purely + /// synchronous code does not need to be protected inside a critical section. + /// Therefore, the critical section is a function that returns a _Future_. + /// If the critical section does not need to return a value, it should be + /// defined as returning `Future`. + /// + /// Returns a _Future_ whose value is the value of the _Future_ returned by + /// the critical section. + /// + /// An exception is thrown if the critical section throws an exception, + /// or an exception is thrown while waiting for the _Future_ returned by + /// the critical section to complete. The lock is released, when those + /// exceptions occur. + + Future protect(Future Function() criticalSection) async { + await acquire(); + try { + return await criticalSection(); + } finally { + release(); + } + } +} diff --git a/packages/mutex/lib/src/read_write_mutex.dart b/packages/mutex/lib/src/read_write_mutex.dart new file mode 100644 index 0000000..8a5c12e --- /dev/null +++ b/packages/mutex/lib/src/read_write_mutex.dart @@ -0,0 +1,304 @@ +part of mutex; + +//################################################################ +/// Internal representation of a request for a lock. +/// +/// This is instantiated for each acquire and, if necessary, it is added +/// to the waiting queue. + +class _ReadWriteMutexRequest { + /// Internal constructor. + /// + /// The [isRead] indicates if this is a request for a read lock (true) or a + /// request for a write lock (false). + + _ReadWriteMutexRequest({required this.isRead}); + + /// Indicates if this is a read or write lock. + + final bool isRead; // true = read lock requested; false = write lock requested + + /// The job's completer. + /// + /// This [Completer] will complete when the job has acquired the lock. + + final Completer completer = Completer(); +} + +//################################################################ +/// Mutual exclusion that supports read and write locks. +/// +/// Multiple read locks can be simultaneously acquired, but at most only +/// one write lock can be acquired at any one time. +/// +/// **Protecting critical code** +/// +/// The [protectWrite] and [protectRead] are convenience methods for acquiring +/// locks and releasing them. Using them will ensure the locks are always +/// released after use. +/// +/// Create the mutex: +/// +/// m = ReadWriteMutex(); +/// +/// Code protected by a write lock: +/// +/// await m.protectWrite(() { +/// // critical write section +/// }); +/// +/// Other code can be protected by a read lock: +/// +/// await m.protectRead(() { +/// // critical read section +/// }); +/// +/// +/// **Explicitly managing locks** +/// +/// Alternatively, the locks can be explicitly acquired and managed. In this +/// situation, the program is responsible for releasing the locks after they +/// have been used. Failure to release the lock will prevent other code for +/// ever acquiring a lock. +/// +/// Create the mutex: +/// +/// m = ReadWriteMutex(); +/// +/// Some code can acquire a write lock: +/// +/// await m.acquireWrite(); +/// try { +/// // critical write section +/// assert(m.isWriteLocked); +/// } finally { +/// m.release(); +/// } +/// +/// Other code can acquire a read lock. +/// +/// await m.acquireRead(); +/// try { +/// // critical read section +/// assert(m.isReadLocked); +/// } finally { +/// m.release(); +/// } +/// +/// The current implementation lets locks be acquired in first-in-first-out +/// order. This ensures there will not be any lock starvation, which can +/// happen if some locks are prioritised over others. Submit a feature +/// request issue, if there is a need for another scheduling algorithm. + +class ReadWriteMutex { + //================================================================ + // Constructors + ReadWriteMutex(); + ReadWriteMutex.writeLocked() : _state = -1; + ReadWriteMutex.readLocked(int? count) : _state = count ?? 1 { + assert(_state > 0, "can't have a negative read lock count"); + } + + //================================================================ + // Members + + /// List of requests waiting for a lock on this mutex. + + final _waiting = <_ReadWriteMutexRequest>[]; + + /// State of the mutex + + int _state = 0; // -1 = write lock, +ve = number of read locks; 0 = no lock + + //================================================================ + // Methods + + /// Indicates if a lock (read or write) has been acquired and not released. + bool get isLocked => _state != 0; + + /// Indicates if a write lock has been acquired and not released. + bool get isWriteLocked => _state == -1; + + /// Indicates if one or more read locks has been acquired and not released. + bool get isReadLocked => 0 < _state; + + /// Indicates the number of waiters on this mutex + int get waiters => _waiting.length; + + /// Acquire a read lock + /// + /// Returns a future that will be completed when the lock has been acquired. + /// + /// A read lock can not be acquired when there is a write lock on the mutex. + /// But it can be acquired if there are other read locks. + /// + /// Consider using the convenience method [protectRead], otherwise the caller + /// is responsible for making sure the lock is released after it is no longer + /// needed. Failure to release the lock means no other code can acquire a + /// write lock. + + Future acquireRead() => _acquire(isRead: true); + + /// Acquire a write lock + /// + /// Returns a future that will be completed when the lock has been acquired. + /// + /// A write lock can only be acquired when there are no other locks (neither + /// read locks nor write locks) on the mutex. + /// + /// Consider using the convenience method [protectWrite], otherwise the caller + /// is responsible for making sure the lock is released after it is no longer + /// needed. Failure to release the lock means no other code can acquire the + /// lock (neither a read lock or a write lock). + + Future acquireWrite() => _acquire(isRead: false); + + /// Release a lock. + /// + /// Release the lock that was previously acquired. + /// + /// When the lock is released, locks waiting to be acquired can be acquired + /// depending on the type of lock waiting and if other locks have been + /// acquired. + /// + /// A [StateError] is thrown if the mutex does not currently have a lock on + /// it. + + void release() { + if (_state == -1) { + // Write lock released + _state = 0; + } else if (0 < _state) { + // Read lock released + _state--; + } else if (_state == 0) { + throw StateError('no lock to release'); + } else { + assert(false, 'invalid state'); + } + + // If there are jobs waiting and the next job can acquire the mutex, + // let it acquire it and remove it from the queue. + // + // This is a while loop, because there could be multiple jobs on the + // queue waiting for a read-only mutex. So they can all be allowed to run. + + while (_waiting.isNotEmpty) { + final nextJob = _waiting.first; + if (_jobAcquired(nextJob)) { + _waiting.removeAt(0); + } else { + // The next job cannot acquire the mutex. This only occurs when: the + // the currently running job has a write mutex (_state == -1); or the + // next job wants write mutex and there is a job currently running + // (regardless of what type of mutex it has acquired). + assert(_state < 0 || !nextJob.isRead, + 'unexpected: next job cannot be acquired'); + break; // no more can be removed from the queue + } + } + } + + /// Convenience method for protecting a function with a read lock. + /// + /// This method guarantees a read lock is always acquired before invoking the + /// [criticalSection] function. It also guarantees the lock is always + /// released. + /// + /// A critical section should always contain asynchronous code, since purely + /// synchronous code does not need to be protected inside a critical section. + /// Therefore, the critical section is a function that returns a _Future_. + /// If the critical section does not need to return a value, it should be + /// defined as returning `Future`. + /// + /// Returns a _Future_ whose value is the value of the _Future_ returned by + /// the critical section. + /// + /// An exception is thrown if the critical section throws an exception, + /// or an exception is thrown while waiting for the _Future_ returned by + /// the critical section to complete. The lock is released, when those + /// exceptions occur. + + Future protectRead(Future Function() criticalSection) async { + await acquireRead(); + try { + return await criticalSection(); + } finally { + release(); + } + } + + /// Convenience method for protecting a function with a write lock. + /// + /// This method guarantees a write lock is always acquired before invoking the + /// [criticalSection] function. It also guarantees the lock is always + /// released. + /// + /// A critical section should always contain asynchronous code, since purely + /// synchronous code does not need to be protected inside a critical section. + /// Therefore, the critical section is a function that returns a _Future_. + /// If the critical section does not need to return a value, it should be + /// defined as returning `Future`. + /// + /// Returns a _Future_ whose value is the value of the _Future_ returned by + /// the critical section. + /// + /// An exception is thrown if the critical section throws an exception, + /// or an exception is thrown while waiting for the _Future_ returned by + /// the critical section to complete. The lock is released, when those + /// exceptions occur. + + Future protectWrite(Future Function() criticalSection) async { + await acquireWrite(); + try { + return await criticalSection(); + } finally { + release(); + } + } + + /// Internal acquire method. + /// + /// Used to acquire a read lock (when [isRead] is true) or a write lock + /// (when [isRead] is false). + /// + /// Returns a Future that completes when the lock has been acquired. + + Future _acquire({required bool isRead}) { + final newJob = _ReadWriteMutexRequest(isRead: isRead); + + if (_waiting.isNotEmpty || !_jobAcquired(newJob)) { + // This new job cannot run yet. There are either other jobs already + // waiting, or there are no waiting jobs but this job cannot start + // because the mutex is currently acquired (namely, either this new job + // or the currently running job is read-write). + // + // Add the new job to the end of the queue. + + _waiting.add(newJob); + } + + return newJob.completer.future; + } + + /// Determine if the [job] can now acquire the lock. + /// + /// If it can acquire the lock, the job's completer is completed, the + /// state updated, and true is returned. If not, false is returned. + /// + /// A job for a read lock can only be acquired if there are no other locks + /// or there are read lock(s). A job for a write lock can only be acquired + /// if there are no other locks. + + bool _jobAcquired(_ReadWriteMutexRequest job) { + assert(-1 <= _state, 'must not be write locked'); + if (_state == 0 || (0 < _state && job.isRead)) { + // Can acquire + _state = (job.isRead) ? (_state + 1) : -1; + job.completer.complete(); + return true; + } else { + return false; + } + } +} diff --git a/packages/mutex/pubspec.yaml b/packages/mutex/pubspec.yaml new file mode 100644 index 0000000..52d86a1 --- /dev/null +++ b/packages/mutex/pubspec.yaml @@ -0,0 +1,12 @@ +name: mutex +description: Mutual exclusion with implementation of normal and read-write mutex +version: 3.1.0 +publish_to: none + +environment: + sdk: '>=2.15.0 <4.0.0' + +dev_dependencies: + lint_hard: ^4.0.0 + pana: ^0.21.37 + test: ^1.16.3 diff --git a/packages/mutex/test/mutex_multiple_read_test.dart b/packages/mutex/test/mutex_multiple_read_test.dart new file mode 100644 index 0000000..5ed8345 --- /dev/null +++ b/packages/mutex/test/mutex_multiple_read_test.dart @@ -0,0 +1,102 @@ +// Test contributed by "Cat-sushi" +// + +import 'dart:async'; +// import 'dart:io'; + +import 'package:mutex/mutex.dart'; +import 'package:test/test.dart'; + +//================================================================ +// For debug output +// +// Uncomment the "stdout.write" line in the [debugWrite] method to enable +// debug output. + +int numReadAcquired = 0; +int numReadReleased = 0; + +enum State { waitingToAcquire, acquired, released } + +const stateSymbol = { + State.waitingToAcquire: '?', + State.acquired: '+', + State.released: '-' +}; + +var _outputCount = 0; // to manage line breaks + +void debugOutput(String id, State state) { + debugWrite('$id${stateSymbol[state]} '); + + _outputCount++; + if (_outputCount % 10 == 0) { + debugWrite('\n'); + } +} + +void debugWrite(String str) { + // Uncomment to show what is happening + // stdout.write(str); +} + +//================================================================ + +Future mySleep([int ms = 1000]) async { + await Future.delayed(Duration(milliseconds: ms)); +} + +Future sharedLoop1(ReadWriteMutex mutex, String symbol) async { + while (true) { + debugOutput(symbol, State.waitingToAcquire); + + await mutex.protectRead(() async { + numReadAcquired++; + debugOutput(symbol, State.acquired); + + await mySleep(100); + }); + numReadReleased++; + + debugOutput(symbol, State.released); + } +} + +void main() { + group('exclusive lock tests', () { + test('test1', () async { + const numReadLoops = 5; + + final mutex = ReadWriteMutex(); + + assert(numReadLoops < 26, 'too many read loops for lowercase letters'); + debugWrite('Number of read loops: $numReadLoops\n'); + + for (var x = 0; x < numReadLoops; x++) { + final symbol = String.fromCharCode('a'.codeUnitAt(0) + x); + unawaited(sharedLoop1(mutex, symbol)); + await mySleep(10); + } + + await mySleep(); + + debugWrite('\nAbout to acquireWrite' + ' (reads: acquired=$numReadAcquired released=$numReadReleased' + ' outstanding=${numReadAcquired - numReadReleased})\n'); + _outputCount = 0; // reset line break + + const writeSymbol = 'W'; + + debugOutput(writeSymbol, State.waitingToAcquire); + await mutex.acquireWrite(); + debugOutput(writeSymbol, State.acquired); + mutex.release(); + debugOutput(writeSymbol, State.released); + + debugWrite('\nWrite mutex released\n'); + _outputCount = 0; // reset line break + + expect('a', 'a'); + }); + }); +} diff --git a/packages/mutex/test/mutex_readwrite_test.dart b/packages/mutex/test/mutex_readwrite_test.dart new file mode 100644 index 0000000..310caa1 --- /dev/null +++ b/packages/mutex/test/mutex_readwrite_test.dart @@ -0,0 +1,486 @@ +import 'dart:async'; +import 'package:mutex/mutex.dart'; +import 'package:test/test.dart'; + +//################################################################ + +class RWTester { + int _operation = 0; + final _operationSequences = []; + + /// Execution sequence of the operations done. + /// + /// Each element corresponds to the position of the initial execution + /// order of the read/write operation future. + List get operationSequences => _operationSequences; + + ReadWriteMutex mutex = ReadWriteMutex(); + + /// Set to true to print out read/write to the balance during deposits + static const bool debugOutput = false; + + final DateTime _startTime = DateTime.now(); + + void _debugPrint(String message) { + if (debugOutput) { + final t = DateTime.now().difference(_startTime).inMilliseconds; + // ignore: avoid_print + print('$t: $message'); + } + } + + void reset() { + _operationSequences.clear(); + _debugPrint('reset'); + } + + /// Waits [startDelay] and then invokes critical section with mutex. + /// + /// Writes to [_operationSequences]. If the readwrite locks are respected + /// then the final state of the list will be in ascending order. + Future writing(int startDelay, int sequence, int endDelay) async { + await Future.delayed(Duration(milliseconds: startDelay)); + + await mutex.protectWrite(() async { + final op = ++_operation; + _debugPrint('[$op] write start: <- $_operationSequences'); + final tmp = _operationSequences; + expect(mutex.isWriteLocked, isTrue); + expect(_operationSequences, orderedEquals(tmp)); + // Add the position of operation to the list of operations. + _operationSequences.add(sequence); // add position to list + expect(mutex.isWriteLocked, isTrue); + await Future.delayed(Duration(milliseconds: endDelay)); + _debugPrint('[$op] write finish: -> $_operationSequences'); + }); + } + + /// Waits [startDelay] and then invokes critical section with mutex. + /// + /// + Future reading(int startDelay, int sequence, int endDelay) async { + await Future.delayed(Duration(milliseconds: startDelay)); + + await mutex.protectRead(() async { + final op = ++_operation; + _debugPrint('[$op] read start: <- $_operationSequences'); + expect(mutex.isReadLocked, isTrue); + _operationSequences.add(sequence); // add position to list + await Future.delayed(Duration(milliseconds: endDelay)); + _debugPrint('[$op] read finish: <- $_operationSequences'); + }); + } +} + +//################################################################ + +//---------------------------------------------------------------- + +void main() { + final account = RWTester(); + + setUp(account.reset); + + test('multiple read locks', () async { + await Future.wait([ + account.reading(0, 1, 1000), + account.reading(0, 2, 900), + account.reading(0, 3, 800), + account.reading(0, 4, 700), + account.reading(0, 5, 600), + account.reading(0, 6, 500), + account.reading(0, 7, 400), + account.reading(0, 8, 300), + account.reading(0, 9, 200), + account.reading(0, 10, 100), + ]); + // The first future acquires the lock first and waits the longest to give it + // up. This should however not block any of the other read operations + // as such the reads should finish in ascending orders. + expect( + account.operationSequences, + orderedEquals([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]), + ); + }); + + test('multiple write locks', () async { + await Future.wait([ + account.writing(0, 1, 100), + account.writing(0, 2, 100), + account.writing(0, 3, 100), + ]); + // The first future writes first and holds the lock until 100 ms + // Even though the second future starts execution, the lock cannot be + // acquired until it is released by the first future. + // Therefore the sequence of operations will be in ascending order + // of the futures. + expect( + account.operationSequences, + orderedEquals([1, 2, 3]), + ); + }); + + test('acquireWrite() before acquireRead()', () async { + const lockTimeout = Duration(milliseconds: 100); + + final mutex = ReadWriteMutex(); + + await mutex.acquireWrite(); + expect(mutex.isReadLocked, equals(false)); + expect(mutex.isWriteLocked, equals(true)); + + // Since there is a write lock existing, a read lock cannot be acquired. + final readLock = mutex.acquireRead().timeout(lockTimeout); + expect( + () async => readLock, + throwsA(isA()), + ); + }); + + test('acquireRead() before acquireWrite()', () async { + const lockTimeout = Duration(milliseconds: 100); + + final mutex = ReadWriteMutex(); + + await mutex.acquireRead(); + expect(mutex.isReadLocked, equals(true)); + expect(mutex.isWriteLocked, equals(false)); + + // Since there is a read lock existing, a write lock cannot be acquired. + final writeLock = mutex.acquireWrite().timeout(lockTimeout); + expect( + () async => writeLock, + throwsA(isA()), + ); + }); + + test('mixture of read write locks execution order', () async { + await Future.wait([ + account.reading(0, 1, 100), + account.reading(10, 2, 100), + account.reading(20, 3, 100), + account.writing(30, 4, 100), + account.writing(40, 5, 100), + account.writing(50, 6, 100), + ]); + + expect( + account.operationSequences, + orderedEquals([1, 2, 3, 4, 5, 6]), + ); + }); + + group('protectRead', () { + test('lock obtained and released on success', () async { + final m = ReadWriteMutex(); + + await m.protectRead(() async { + // critical section + expect(m.isLocked, isTrue); + }); + expect(m.isLocked, isFalse); + }); + + test('value returned from critical section', () async { + // These are the normal scenario of the critical section running + // successfully. It tests different return types from the + // critical section. + + final m = ReadWriteMutex(); + + // returns Future + await m.protectRead(() async {}); + + // returns Future + final number = await m.protectRead(() async => 42); + expect(number, equals(42)); + + // returns Future completes with value + final optionalNumber = await m.protectRead(() async => 1024); + expect(optionalNumber, equals(1024)); + + // returns Future completes with null + final optionalNumberNull = await m.protectRead(() async => null); + expect(optionalNumberNull, isNull); + + // returns Future + final word = await m.protectRead(() async => 'foobar'); + expect(word, equals('foobar')); + + // returns Future completes with value + final optionalWord = await m.protectRead(() async => 'baz'); + expect(optionalWord, equals('baz')); + + // returns Future completes with null + final optionalWordNull = await m.protectRead(() async => null); + expect(optionalWordNull, isNull); + + expect(m.isLocked, isFalse); + }); + + test('exception in synchronous code', () async { + // Tests what happens when an exception is raised in the **synchronous** + // part of the critical section. + // + // Locks are correctly managed: the lock is obtained before executing + // the critical section, and is released when the exception is thrown + // by the _protect_ method. + // + // The exception is raised when waiting for the Future returned by + // _protect_ to complete. Even though the exception is synchronously + // raised by the critical section, it won't be thrown when _protect_ + // is invoked. The _protect_ method always successfully returns a + // _Future_. + + Future criticalSection() { + final c = Completer()..complete(42); + + // synchronous exception + throw const FormatException('synchronous exception'); + // ignore: dead_code + return c.future; + } + + // Check the criticalSection behaves as expected for the test + + try { + // ignore: unused_local_variable + final resultFuture = criticalSection(); + fail('critical section did not throw synchronous exception'); + } on FormatException { + // expected: invoking the criticalSection results in the exception + } + + final m = ReadWriteMutex(); + + try { + // Invoke protect to get the Future (this should succeed) + final resultFuture = m.protectRead(criticalSection); + expect(resultFuture, isA>()); + + // Wait for the Future (this should fail) + final result = await resultFuture; + expect(result, isNotNull); + fail('exception not thrown'); + } on FormatException catch (e) { + expect(m.isLocked, isFalse); + expect(e.message, equals('synchronous exception')); + } + + expect(m.isLocked, isFalse); + }); + + test('exception in asynchronous code', () async { + // Tests what happens when an exception is raised in the **asynchronous** + // part of the critical section. + // + // Locks are correctly managed: the lock is obtained before executing + // the critical section, and is released when the exception is thrown + // by the _protect_ method. + // + // The exception is raised when waiting for the Future returned by + // _protect_ to complete. + + Future criticalSection() async { + final c = Completer()..complete(42); + + await Future.delayed(const Duration(seconds: 1), () {}); + + // asynchronous exception (since it must wait for the above line) + throw const FormatException('asynchronous exception'); + // ignore: dead_code + return c.future; + } + + // Check the criticalSection behaves as expected for the test + + final resultFuture = criticalSection(); + expect(resultFuture, isA>()); + // invoking the criticalSection does not result in the exception + try { + await resultFuture; + fail('critical section did not throw asynchronous exception'); + } on FormatException { + // expected: exception happens on the await + } + + final m = ReadWriteMutex(); + + try { + // Invoke protect to get the Future (this should succeed) + final resultFuture = m.protectRead(criticalSection); + expect(resultFuture, isA>()); + + // Even though the criticalSection throws the exception in synchronous + // code, protect causes it to become an asynchronous exception. + + // Wait for the Future (this should fail) + final result = await resultFuture; + expect(result, isNotNull); + fail('exception not thrown'); + } on FormatException catch (e) { + expect(m.isLocked, isFalse); + expect(e.message, equals('asynchronous exception')); + } + + expect(m.isLocked, isFalse); + }); + }); + + group('protectWrite', () { + test('lock obtained and released on success', () async { + final m = ReadWriteMutex(); + + await m.protectWrite(() async { + // critical section + expect(m.isLocked, isTrue); + }); + expect(m.isLocked, isFalse); + }); + + test('value returned from critical section', () async { + // These are the normal scenario of the critical section running + // successfully. It tests different return types from the + // critical section. + + final m = ReadWriteMutex(); + + // returns Future + await m.protectWrite(() async {}); + + // returns Future + final number = await m.protectWrite(() async => 42); + expect(number, equals(42)); + + // returns Future completes with value + final optionalNumber = await m.protectWrite(() async => 1024); + expect(optionalNumber, equals(1024)); + + // returns Future completes with null + final optionalNumberNull = await m.protectWrite(() async => null); + expect(optionalNumberNull, isNull); + + // returns Future + final word = await m.protectWrite(() async => 'foobar'); + expect(word, equals('foobar')); + + // returns Future completes with value + final optionalWord = await m.protectWrite(() async => 'baz'); + expect(optionalWord, equals('baz')); + + // returns Future completes with null + final optionalWordNull = await m.protectWrite(() async => null); + expect(optionalWordNull, isNull); + + expect(m.isLocked, isFalse); + }); + + test('exception in synchronous code', () async { + // Tests what happens when an exception is raised in the **synchronous** + // part of the critical section. + // + // Locks are correctly managed: the lock is obtained before executing + // the critical section, and is released when the exception is thrown + // by the _protect_ method. + // + // The exception is raised when waiting for the Future returned by + // _protect_ to complete. Even though the exception is synchronously + // raised by the critical section, it won't be thrown when _protect_ + // is invoked. The _protect_ method always successfully returns a + // _Future_. + + Future criticalSection() { + final c = Completer()..complete(42); + + // synchronous exception + throw const FormatException('synchronous exception'); + // ignore: dead_code + return c.future; + } + + // Check the criticalSection behaves as expected for the test + + try { + // ignore: unused_local_variable + final resultFuture = criticalSection(); + fail('critical section did not throw synchronous exception'); + } on FormatException { + // expected: invoking the criticalSection results in the exception + } + + final m = ReadWriteMutex(); + + try { + // Invoke protect to get the Future (this should succeed) + final resultFuture = m.protectWrite(criticalSection); + expect(resultFuture, isA>()); + + // Wait for the Future (this should fail) + final result = await resultFuture; + expect(result, isNotNull); + fail('exception not thrown'); + } on FormatException catch (e) { + expect(m.isLocked, isFalse); + expect(e.message, equals('synchronous exception')); + } + + expect(m.isLocked, isFalse); + }); + + test('exception in asynchronous code', () async { + // Tests what happens when an exception is raised in the **asynchronous** + // part of the critical section. + // + // Locks are correctly managed: the lock is obtained before executing + // the critical section, and is released when the exception is thrown + // by the _protect_ method. + // + // The exception is raised when waiting for the Future returned by + // _protect_ to complete. + + Future criticalSection() async { + final c = Completer()..complete(42); + + await Future.delayed(const Duration(seconds: 1), () {}); + + // asynchronous exception (since it must wait for the above line) + throw const FormatException('asynchronous exception'); + // ignore: dead_code + return c.future; + } + + // Check the criticalSection behaves as expected for the test + + final resultFuture = criticalSection(); + expect(resultFuture, isA>()); + // invoking the criticalSection does not result in the exception + try { + await resultFuture; + fail('critical section did not throw asynchronous exception'); + } on FormatException { + // expected: exception happens on the await + } + + final m = ReadWriteMutex(); + + try { + // Invoke protect to get the Future (this should succeed) + final resultFuture = m.protectWrite(criticalSection); + expect(resultFuture, isA>()); + + // Even though the criticalSection throws the exception in synchronous + // code, protect causes it to become an asynchronous exception. + + // Wait for the Future (this should fail) + final result = await resultFuture; + expect(result, isNotNull); + fail('exception not thrown'); + } on FormatException catch (e) { + expect(m.isLocked, isFalse); + expect(e.message, equals('asynchronous exception')); + } + + expect(m.isLocked, isFalse); + }); + }); +} diff --git a/packages/mutex/test/mutex_test.dart b/packages/mutex/test/mutex_test.dart new file mode 100644 index 0000000..0db5f52 --- /dev/null +++ b/packages/mutex/test/mutex_test.dart @@ -0,0 +1,341 @@ +import 'dart:async'; +import 'package:mutex/mutex.dart'; +import 'package:test/test.dart'; + +//################################################################ +/// Account simulating the classic "simultaneous update" concurrency problem. +/// +/// The deposit operation reads the balance, waits for a short time (where +/// problems can occur if the balance is changed) and then writes out the +/// new balance. +/// +class Account { + int get balance => _balance; + int _balance = 0; + + int _operation = 0; + + Mutex mutex = Mutex(); + + /// Set to true to print out read/write to the balance during deposits + static const bool debugOutput = false; + + /// Time used for calculating time offsets in debug messages. + final DateTime _startTime = DateTime.now(); + + void _debugPrint(String message) { + if (debugOutput) { + final t = DateTime.now().difference(_startTime).inMilliseconds; + // ignore: avoid_print + print('$t: $message'); + } + } + + void reset([int startingBalance = 0]) { + _balance = startingBalance; + _debugPrint('reset: balance = $_balance'); + } + + /// Waits [startDelay] and then invokes critical section without mutex. + /// + Future depositUnsafe( + int amount, int startDelay, int dangerWindow) async { + await Future.delayed(Duration(milliseconds: startDelay)); + + await _depositCriticalSection(amount, dangerWindow); + } + + /// Waits [startDelay] and then invokes critical section with mutex. + /// + Future depositWithMutex( + int amount, int startDelay, int dangerWindow) async { + await Future.delayed(Duration(milliseconds: startDelay)); + + await mutex.acquire(); + try { + expect(mutex.isLocked, isTrue); + await _depositCriticalSection(amount, dangerWindow); + expect(mutex.isLocked, isTrue); + } finally { + mutex.release(); + } + } + + /// Critical section of adding [amount] to the balance. + /// + /// Reads the balance, then sleeps for [dangerWindow] milliseconds, before + /// saving the new balance. If not protected, another invocation of this + /// method while it is sleeping will read the balance before it is updated. + /// The one that saves its balance last will overwrite the earlier saved + /// balances (effectively those other deposits will be lost). + /// + Future _depositCriticalSection(int amount, int dangerWindow) async { + final op = ++_operation; + + _debugPrint('[$op] read balance: $_balance'); + + final tmp = _balance; + + await Future.delayed(Duration(milliseconds: dangerWindow)); + + _balance = tmp + amount; + + _debugPrint('[$op] write balance: $_balance (= $tmp + $amount)'); + } +} + +//################################################################ + +//---------------------------------------------------------------- + +void main() { + const correctBalance = 68; + + final account = Account(); + + test('without mutex', () async { + // First demonstrate that without mutex incorrect results are produced. + + // Without mutex produces incorrect result + // 000. a reads 0 + // 025. b reads 0 + // 050. a writes 42 + // 075. b writes 26 + account.reset(); + await Future.wait([ + account.depositUnsafe(42, 0, 50), + account.depositUnsafe(26, 25, 50) // result overwrites first deposit + ]); + expect(account.balance, equals(26)); // incorrect: first deposit lost + + // Without mutex produces incorrect result + // 000. b reads 0 + // 025. a reads 0 + // 050. b writes 26 + // 075. a writes 42 + account.reset(); + await Future.wait([ + account.depositUnsafe(42, 25, 50), // result overwrites second deposit + account.depositUnsafe(26, 0, 50) + ]); + expect(account.balance, equals(42)); // incorrect: second deposit lost + }); + + test('with mutex', () async { +// Test correct results are produced with mutex + + // With mutex produces correct result + // 000. a acquires lock + // 000. a reads 0 + // 025. b is blocked + // 050. a writes 42 + // 050. a releases lock + // 050. b acquires lock + // 050. b reads 42 + // 100. b writes 68 + account.reset(); + await Future.wait([ + account.depositWithMutex(42, 0, 50), + account.depositWithMutex(26, 25, 50) + ]); + expect(account.balance, equals(correctBalance)); + + // With mutex produces correct result + // 000. b acquires lock + // 000. b reads 0 + // 025. a is blocked + // 050. b writes 26 + // 050. b releases lock + // 050. a acquires lock + // 050. a reads 26 + // 100. a writes 68 + account.reset(); + await Future.wait([ + account.depositWithMutex(42, 25, 50), + account.depositWithMutex(26, 0, 50) + ]); + expect(account.balance, equals(correctBalance)); + }); + + test('multiple acquires are serialized', () async { + // Demonstrate that sections running in a mutex are effectively serialized + const delay = 200; // milliseconds + account.reset(); + await Future.wait([ + account.depositWithMutex(1, 0, delay), + account.depositWithMutex(1, 0, delay), + account.depositWithMutex(1, 0, delay), + account.depositWithMutex(1, 0, delay), + account.depositWithMutex(1, 0, delay), + account.depositWithMutex(1, 0, delay), + account.depositWithMutex(1, 0, delay), + account.depositWithMutex(1, 0, delay), + account.depositWithMutex(1, 0, delay), + account.depositWithMutex(1, 0, delay), + ]); + expect(account.balance, equals(10)); + }); + + group('protect', () { + test('lock obtained and released on success', () async { + // This is the normal scenario of the critical section running + // successfully. The lock is acquired before running the critical + // section, and it is released after it runs (and will remain + // unlocked after the _protect_ method returns). + + final m = Mutex(); + + await m.protect(() async { + // critical section: returns Future + expect(m.isLocked, isTrue); + }); + + expect(m.isLocked, isFalse); + }); + + test('value returned from critical section', () async { + // These are the normal scenario of the critical section running + // successfully. It tests different return types from the + // critical section. + + final m = Mutex(); + + // returns Future + await m.protect(() async {}); + + // returns Future + final number = await m.protect(() async => 42); + expect(number, equals(42)); + + // returns Future completes with value + final optionalNumber = await m.protect(() async => 1024); + expect(optionalNumber, equals(1024)); + + // returns Future completes with null + final optionalNumberNull = await m.protect(() async => null); + expect(optionalNumberNull, isNull); + + // returns Future + final word = await m.protect(() async => 'foobar'); + expect(word, equals('foobar')); + + // returns Future completes with value + final optionalWord = await m.protect(() async => 'baz'); + expect(optionalWord, equals('baz')); + + // returns Future completes with null + final optionalWordNull = await m.protect(() async => null); + expect(optionalWordNull, isNull); + + expect(m.isLocked, isFalse); + }); + + test('exception in synchronous code', () async { + // Tests what happens when an exception is raised in the **synchronous** + // part of the critical section. + // + // Locks are correctly managed: the lock is obtained before executing + // the critical section, and is released when the exception is thrown + // by the _protect_ method. + // + // The exception is raised when waiting for the Future returned by + // _protect_ to complete. Even though the exception is synchronously + // raised by the critical section, it won't be thrown when _protect_ + // is invoked. The _protect_ method always successfully returns a + // _Future_. + + Future criticalSection() { + final c = Completer()..complete(42); + + // synchronous exception + throw const FormatException('synchronous exception'); + // ignore: dead_code + return c.future; + } + + // Check the criticalSection behaves as expected for the test + + try { + // ignore: unused_local_variable + final resultFuture = criticalSection(); + fail('critical section did not throw synchronous exception'); + } on FormatException { + // expected: invoking the criticalSection results in the exception + } + + final m = Mutex(); + + try { + // Invoke protect to get the Future (this should succeed) + final resultFuture = m.protect(criticalSection); + expect(resultFuture, isA>()); + + // Wait for the Future (this should fail) + final result = await resultFuture; + expect(result, isNotNull); + fail('exception not thrown'); + } on FormatException catch (e) { + expect(m.isLocked, isFalse); + expect(e.message, equals('synchronous exception')); + } + + expect(m.isLocked, isFalse); + }); + + test('exception in asynchronous code', () async { + // Tests what happens when an exception is raised in the **asynchronous** + // part of the critical section. + // + // Locks are correctly managed: the lock is obtained before executing + // the critical section, and is released when the exception is thrown + // by the _protect_ method. + // + // The exception is raised when waiting for the Future returned by + // _protect_ to complete. + + Future criticalSection() async { + final c = Completer()..complete(42); + + await Future.delayed(const Duration(seconds: 1), () {}); + + // asynchronous exception (since it must wait for the above line) + throw const FormatException('asynchronous exception'); + // ignore: dead_code + return c.future; + } + + // Check the criticalSection behaves as expected for the test + + final resultFuture = criticalSection(); + expect(resultFuture, isA>()); + // invoking the criticalSection does not result in the exception + try { + await resultFuture; + fail('critical section did not throw asynchronous exception'); + } on FormatException { + // expected: exception happens on the await + } + + final m = Mutex(); + + try { + // Invoke protect to get the Future (this should succeed) + final resultFuture = m.protect(criticalSection); + expect(resultFuture, isA>()); + + // Even though the criticalSection throws the exception in synchronous + // code, protect causes it to become an asynchronous exception. + + // Wait for the Future (this should fail) + final result = await resultFuture; + expect(result, isNotNull); + fail('exception not thrown'); + } on FormatException catch (e) { + expect(m.isLocked, isFalse); + expect(e.message, equals('asynchronous exception')); + } + + expect(m.isLocked, isFalse); + }); + }); +} diff --git a/packages/veilid_support/lib/dht_support/src/dht_record_cubit.dart b/packages/veilid_support/lib/dht_support/src/dht_record_cubit.dart index 20fdf08..86806cf 100644 --- a/packages/veilid_support/lib/dht_support/src/dht_record_cubit.dart +++ b/packages/veilid_support/lib/dht_support/src/dht_record_cubit.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'dart:typed_data'; +import 'package:async_tools/async_tools.dart'; import 'package:bloc/bloc.dart'; import '../../veilid_support.dart'; diff --git a/packages/veilid_support/lib/dht_support/src/dht_record_pool.dart b/packages/veilid_support/lib/dht_support/src/dht_record_pool.dart index fd47da4..ee4b426 100644 --- a/packages/veilid_support/lib/dht_support/src/dht_record_pool.dart +++ b/packages/veilid_support/lib/dht_support/src/dht_record_pool.dart @@ -1,5 +1,6 @@ import 'dart:async'; +import 'package:async_tools/async_tools.dart'; import 'package:equatable/equatable.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; diff --git a/packages/veilid_support/lib/dht_support/src/dht_short_array_cubit.dart b/packages/veilid_support/lib/dht_support/src/dht_short_array_cubit.dart index 71d1d38..afccbde 100644 --- a/packages/veilid_support/lib/dht_support/src/dht_short_array_cubit.dart +++ b/packages/veilid_support/lib/dht_support/src/dht_short_array_cubit.dart @@ -1,5 +1,6 @@ import 'dart:async'; +import 'package:async_tools/async_tools.dart'; import 'package:bloc/bloc.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; diff --git a/packages/veilid_support/lib/src/async_table_db_backed_cubit.dart b/packages/veilid_support/lib/src/async_table_db_backed_cubit.dart index ab1c3a8..a50d893 100644 --- a/packages/veilid_support/lib/src/async_table_db_backed_cubit.dart +++ b/packages/veilid_support/lib/src/async_table_db_backed_cubit.dart @@ -1,8 +1,9 @@ import 'dart:async'; +import 'package:async_tools/async_tools.dart'; import 'package:bloc/bloc.dart'; -import '../veilid_support.dart'; +import 'table_db.dart'; abstract class AsyncTableDBBackedCubit extends Cubit> with TableDBBacked { diff --git a/packages/veilid_support/lib/veilid_support.dart b/packages/veilid_support/lib/veilid_support.dart index 309f118..f873397 100644 --- a/packages/veilid_support/lib/veilid_support.dart +++ b/packages/veilid_support/lib/veilid_support.dart @@ -6,10 +6,7 @@ library veilid_support; export 'package:veilid/veilid.dart'; export 'dht_support/dht_support.dart'; -export 'src/async_tag_lock.dart'; -export 'src/async_value.dart'; export 'src/config.dart'; -export 'src/future_cubit.dart'; export 'src/identity.dart'; export 'src/json_tools.dart'; export 'src/protobuf_tools.dart'; diff --git a/packages/veilid_support/pubspec.lock b/packages/veilid_support/pubspec.lock index ec62d29..54c4755 100644 --- a/packages/veilid_support/pubspec.lock +++ b/packages/veilid_support/pubspec.lock @@ -33,6 +33,13 @@ packages: url: "https://pub.dev" source: hosted version: "2.11.0" + async_tools: + dependency: "direct main" + description: + path: "../async_tools" + relative: true + source: path + version: "1.0.0" bloc: dependency: "direct main" description: @@ -404,12 +411,11 @@ packages: source: hosted version: "1.0.4" mutex: - dependency: "direct main" + dependency: transitive description: - name: mutex - sha256: "8827da25de792088eb33e572115a5eb0d61d61a3c01acbc8bcbe76ed78f1a1f2" - url: "https://pub.dev" - source: hosted + path: "../mutex" + relative: true + source: path version: "3.1.0" node_preamble: dependency: transitive @@ -784,5 +790,5 @@ packages: source: hosted version: "3.1.2" sdks: - dart: ">=3.2.0-194.0.dev <4.0.0" + dart: ">=3.2.6 <4.0.0" flutter: ">=3.10.6" diff --git a/packages/veilid_support/pubspec.yaml b/packages/veilid_support/pubspec.yaml index 471d00b..b1d21c1 100644 --- a/packages/veilid_support/pubspec.yaml +++ b/packages/veilid_support/pubspec.yaml @@ -7,6 +7,8 @@ environment: sdk: '>=3.0.5 <4.0.0' dependencies: + async_tools: + path: ../async_tools bloc: ^8.1.2 equatable: ^2.0.5 fast_immutable_collections: ^9.1.5 @@ -14,7 +16,6 @@ dependencies: json_annotation: ^4.8.1 loggy: ^2.0.3 meta: ^1.10.0 - mutex: ^3.1.0 protobuf: ^3.0.0 veilid: # veilid: ^0.0.1 diff --git a/pubspec.lock b/pubspec.lock index 648e896..1d4800b 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -57,6 +57,13 @@ packages: url: "https://pub.dev" source: hosted version: "2.11.0" + async_tools: + dependency: "direct main" + description: + path: "packages/async_tools" + relative: true + source: path + version: "1.0.0" awesome_extensions: dependency: "direct main" description: @@ -832,10 +839,9 @@ packages: mutex: dependency: "direct main" description: - name: mutex - sha256: "8827da25de792088eb33e572115a5eb0d61d61a3c01acbc8bcbe76ed78f1a1f2" - url: "https://pub.dev" - source: hosted + path: "packages/mutex" + relative: true + source: path version: "3.1.0" nested: dependency: transitive @@ -1609,5 +1615,5 @@ packages: source: hosted version: "0.9.0" sdks: - dart: ">=3.2.3 <4.0.0" + dart: ">=3.2.6 <4.0.0" flutter: ">=3.16.6" diff --git a/pubspec.yaml b/pubspec.yaml index a5f22bc..f2ed0f5 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -11,6 +11,8 @@ dependencies: animated_theme_switcher: ^2.0.10 ansicolor: ^2.0.2 archive: ^3.4.10 + async_tools: + path: packages/async_tools awesome_extensions: ^2.0.12 badges: ^3.1.2 basic_utils: ^5.7.0 @@ -51,7 +53,8 @@ dependencies: meta: ^1.10.0 mobile_scanner: ^3.5.7 motion_toast: ^2.8.0 - mutex: ^3.1.0 + mutex: + path: packages/mutex pasteboard: ^0.2.0 path: ^1.8.3 path_provider: ^2.1.2 From ff14969ffafdcabdc707b09d1bfb32b6d4ad80d2 Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Sun, 11 Feb 2024 14:17:10 -0500 Subject: [PATCH 32/68] more messages work --- lib/chat/views/chat_component.dart | 299 +++++++++--------- .../active_conversation_messages_cubit.dart | 2 + .../chat_single_contact_item_widget.dart | 4 - .../views/contact_invitation_display.dart | 2 +- .../views/invite_dialog.dart | 4 +- lib/layout/home/home.dart | 4 +- .../home/home_account_ready/chat_only.dart | 13 +- .../home_account_ready.dart | 12 +- .../main_pager/main_pager.dart | 2 +- lib/layout/home/home_no_active.dart | 2 +- lib/tools/widget_helpers.dart | 60 ++-- .../async_tools/lib/src/single_async.dart | 14 +- 12 files changed, 226 insertions(+), 192 deletions(-) diff --git a/lib/chat/views/chat_component.dart b/lib/chat/views/chat_component.dart index 9076a49..ac4fc81 100644 --- a/lib/chat/views/chat_component.dart +++ b/lib/chat/views/chat_component.dart @@ -18,55 +18,94 @@ import '../../theme/theme.dart'; import '../../tools/tools.dart'; import '../chat.dart'; -class ChatComponent extends StatefulWidget { - const ChatComponent({required this.remoteConversationRecordKey, super.key}); +class ChatComponent extends StatelessWidget { + const ChatComponent._( + {required TypedKey localUserIdentityKey, + required TypedKey remoteConversationRecordKey, + required IList messages, + required types.User localUser, + required types.User remoteUser, + super.key}) + : _localUserIdentityKey = localUserIdentityKey, + _remoteConversationRecordKey = remoteConversationRecordKey, + _messages = messages, + _localUser = localUser, + _remoteUser = remoteUser; - @override - ChatComponentState createState() => ChatComponentState(); + final TypedKey _localUserIdentityKey; + final TypedKey _remoteConversationRecordKey; + final IList _messages; + final types.User _localUser; + final types.User _remoteUser; - final TypedKey remoteConversationRecordKey; + // Builder wrapper function that takes care of state management requirements + static Widget builder( + {required TypedKey remoteConversationRecordKey, Key? key}) => + Builder(builder: (context) { + // Get all watched dependendies + final activeAccountInfo = context.watch(); + final accountRecordInfo = + context.watch().state.data?.value; + if (accountRecordInfo == null) { + return debugPage('should always have an account record here'); + } + final contactList = context.watch().state.data?.value; + if (contactList == null) { + return debugPage('should always have a contact list here'); + } + final avconversation = context.select?>( + (x) => x.state[remoteConversationRecordKey]); + if (avconversation == null) { + return debugPage('should always have an active conversation here'); + } + final conversation = avconversation.data?.value; + if (conversation == null) { + return avconversation.buildNotData(); + } - @override - void debugFillProperties(DiagnosticPropertiesBuilder properties) { - super.debugFillProperties(properties); - properties.add(DiagnosticsProperty( - 'chatRemoteConversationKey', remoteConversationRecordKey)); - } -} + // Make flutter_chat_ui 'User's + final localUserIdentityKey = activeAccountInfo + .localAccount.identityMaster + .identityPublicTypedKey(); -class ChatComponentState extends State { - final _unfocusNode = FocusNode(); - late final types.User _localUser; - late final types.User _remoteUser; + final localUser = types.User( + id: localUserIdentityKey.toString(), + firstName: accountRecordInfo.profile.name, + ); + final editedName = conversation.contact.editedProfile.name; + final remoteUser = types.User( + id: proto.TypedKeyProto.fromProto( + conversation.contact.identityPublicKey) + .toString(), + firstName: editedName); - @override - void initState() { - super.initState(); + // Get the messages to display + // and ensure it is safe to operate() on the MessageCubit for this chat + final avmessages = context.select>?>( + (x) => x.state[remoteConversationRecordKey]); + if (avmessages == null) { + return waitingPage(); + } + final messages = avmessages.data?.value; + if (messages == null) { + return avmessages.buildNotData(); + } - _localUser = types.User( - id: widget.activeAccountInfo.localAccount.identityMaster - .identityPublicTypedKey() - .toString(), - firstName: widget.activeAccountInfo.account.profile.name, - ); - _remoteUser = types.User( - id: proto.TypedKeyProto.fromProto( - widget.activeChatContact.identityPublicKey) - .toString(), - firstName: widget.activeChatContact.remoteProfile.name); - } + return ChatComponent._( + localUserIdentityKey: localUserIdentityKey, + remoteConversationRecordKey: remoteConversationRecordKey, + messages: messages, + localUser: localUser, + remoteUser: remoteUser, + key: key); + }); - @override - void dispose() { - _unfocusNode.dispose(); - super.dispose(); - } + ///////////////////////////////////////////////////////////////////// - types.Message protoMessageToMessage(proto.Message message) { - final isLocal = message.author == - widget.activeAccountInfo.localAccount.identityMaster - .identityPublicTypedKey() - .toProto(); + types.Message messageToChatMessage(proto.Message message) { + final isLocal = message.author == _localUserIdentityKey.toProto(); final textMessage = types.TextMessage( author: isLocal ? _localUser : _remoteUser, @@ -77,142 +116,98 @@ class ChatComponentState extends State { return textMessage; } - Future _addMessage(proto.Message protoMessage) async { - if (protoMessage.text.isEmpty) { + Future _addMessage(BuildContext context, proto.Message message) async { + if (message.text.isEmpty) { return; } - - final message = protoMessageToMessage(protoMessage); - - // setState(() { - // _messages.insert(0, message); - // }); - - // Now add the message to the conversation messages - final localConversationRecordKey = proto.TypedKeyProto.fromProto( - widget.activeChatContact.localConversationRecordKey); - final remoteIdentityPublicKey = proto.TypedKeyProto.fromProto( - widget.activeChatContact.identityPublicKey); - - await addLocalConversationMessage( - activeAccountInfo: widget.activeAccountInfo, - localConversationRecordKey: localConversationRecordKey, - remoteIdentityPublicKey: remoteIdentityPublicKey, - message: protoMessage); - - ref.invalidate(activeConversationMessagesProvider); + await context.read().operate( + _remoteConversationRecordKey, + closure: (messagesCubit) => messagesCubit.addMessage(message: message)); } - Future _handleSendPressed(types.PartialText message) async { + Future _handleSendPressed( + BuildContext context, types.PartialText message) async { final protoMessage = proto.Message() - ..author = widget.activeAccountInfo.localAccount.identityMaster - .identityPublicTypedKey() - .toProto() - ..timestamp = (await eventualVeilid.future).now().toInt64() + ..author = _localUserIdentityKey.toProto() + ..timestamp = Veilid.instance.now().toInt64() ..text = message.text; //..signature = signature; - await _addMessage(protoMessage); + await _addMessage(context, protoMessage); } - void _handleAttachmentPressed() { + Future _handleAttachmentPressed() async { // } @override - // ignore: prefer_expression_function_bodies Widget build(BuildContext context) { final theme = Theme.of(context); final scale = theme.extension()!; final textTheme = Theme.of(context).textTheme; final chatTheme = makeChatTheme(scale, textTheme); - final contactListCubit = context.watch(); + // Convert protobuf messages to chat messages + final chatMessages = []; + for (final message in _messages) { + final chatMessage = messageToChatMessage(message); + chatMessages.insert(0, chatMessage); + } - return contactListCubit.state.builder((context, contactList) { - // Get active chat contact profile - final activeChatContactIdx = contactList.indexWhere((c) => - widget.remoteConversationRecordKey == c.remoteConversationRecordKey); - late final proto.Contact activeChatContact; - if (activeChatContactIdx == -1) { - // xxx: error, no contact for conversation... - return const NoConversationWidget(); - } else { - activeChatContact = contactList[activeChatContactIdx]; - } - final contactName = activeChatContact.editedProfile.name; - - final messages = context.select>?>( - (x) => x.state[widget.remoteConversationRecordKey]); - if (messages == null) { - // xxx: error, no messages for conversation... - return const NoConversationWidget(); - } - return messages.builder((context, protoMessages) { - final messages = []; - for (final protoMessage in protoMessages) { - final message = protoMessageToMessage(protoMessage); - messages.insert(0, message); - } - return DefaultTextStyle( - style: textTheme.bodySmall!, - child: Align( - alignment: AlignmentDirectional.centerEnd, - child: Stack( + return DefaultTextStyle( + style: textTheme.bodySmall!, + child: Align( + alignment: AlignmentDirectional.centerEnd, + child: Stack( + children: [ + Column( children: [ - Column( - children: [ - Container( - height: 48, - decoration: BoxDecoration( - color: scale.primaryScale.subtleBorder, - ), - child: Row(children: [ - Align( - alignment: AlignmentDirectional.centerStart, - child: Padding( - padding: const EdgeInsetsDirectional.fromSTEB( - 16, 0, 16, 0), - child: Text(contactName, - textAlign: TextAlign.start, - style: textTheme.titleMedium), - )), - const Spacer(), - IconButton( - icon: const Icon(Icons.close), - onPressed: () async { - context - .read() - .setActiveChat(null); - }).paddingLTRB(16, 0, 16, 0) - ]), + Container( + height: 48, + decoration: BoxDecoration( + color: scale.primaryScale.subtleBorder, + ), + child: Row(children: [ + Align( + alignment: AlignmentDirectional.centerStart, + child: Padding( + padding: const EdgeInsetsDirectional.fromSTEB( + 16, 0, 16, 0), + child: Text(_remoteUser.firstName!, + textAlign: TextAlign.start, + style: textTheme.titleMedium), + )), + const Spacer(), + IconButton( + icon: const Icon(Icons.close), + onPressed: () async { + context.read().setActiveChat(null); + }).paddingLTRB(16, 0, 16, 0) + ]), + ), + Expanded( + child: DecoratedBox( + decoration: const BoxDecoration(), + child: Chat( + theme: chatTheme, + messages: chatMessages, + //onAttachmentPressed: _handleAttachmentPressed, + //onMessageTap: _handleMessageTap, + //onPreviewDataFetched: _handlePreviewDataFetched, + onSendPressed: (message) { + singleFuture(this, + () async => _handleSendPressed(context, message)); + }, + //showUserAvatars: false, + //showUserNames: true, + user: _localUser, ), - Expanded( - child: DecoratedBox( - decoration: const BoxDecoration(), - child: Chat( - theme: chatTheme, - messages: messages, - //onAttachmentPressed: _handleAttachmentPressed, - //onMessageTap: _handleMessageTap, - //onPreviewDataFetched: _handlePreviewDataFetched, - - onSendPressed: (message) { - unawaited(_handleSendPressed(message)); - }, - //showUserAvatars: false, - //showUserNames: true, - user: _localUser, - ), - ), - ), - ], + ), ), ], ), - )); - }); - }); + ], + ), + )); } } diff --git a/lib/chat_list/cubits/active_conversation_messages_cubit.dart b/lib/chat_list/cubits/active_conversation_messages_cubit.dart index d7db7a5..18c4c6e 100644 --- a/lib/chat_list/cubits/active_conversation_messages_cubit.dart +++ b/lib/chat_list/cubits/active_conversation_messages_cubit.dart @@ -100,6 +100,8 @@ class ActiveConversationMessagesCubit extends BlocMapCubit()!; final activeChatCubit = context.watch(); - // final activeConversation = context.select(); - // final activeConversationMessagesCubit = - // context.watch(); xxx does this need to be here? - final remoteConversationRecordKey = proto.TypedKeyProto.fromProto(_contact.remoteConversationRecordKey); final selected = activeChatCubit.state == remoteConversationRecordKey; diff --git a/lib/contact_invitation/views/contact_invitation_display.dart b/lib/contact_invitation/views/contact_invitation_display.dart index 7b6fed5..f54994e 100644 --- a/lib/contact_invitation/views/contact_invitation_display.dart +++ b/lib/contact_invitation/views/contact_invitation_display.dart @@ -89,7 +89,7 @@ class ContactInvitationDisplayDialogState minHeight: cardsize, maxHeight: cardsize), child: signedContactInvitationBytesV.when( - loading: () => buildProgressIndicator(context), + loading: buildProgressIndicator, data: (data) => Form( key: formKey, child: Column(children: [ diff --git a/lib/contact_invitation/views/invite_dialog.dart b/lib/contact_invitation/views/invite_dialog.dart index f09a1e5..f4fe055 100644 --- a/lib/contact_invitation/views/invite_dialog.dart +++ b/lib/contact_invitation/views/invite_dialog.dart @@ -242,7 +242,7 @@ class InviteDialogState extends State { return SizedBox( height: 300, width: 300, - child: buildProgressIndicator(context).toCenter()) + child: buildProgressIndicator().toCenter()) .paddingAll(16); } return ConstrainedBox( @@ -258,7 +258,7 @@ class InviteDialogState extends State { Column(children: [ Text(translate('invite_dialog.validating')) .paddingLTRB(0, 0, 0, 16), - buildProgressIndicator(context).paddingAll(16), + buildProgressIndicator().paddingAll(16), ]).toCenter(), if (_validInvitation == null && !_isValidating && diff --git a/lib/layout/home/home.dart b/lib/layout/home/home.dart index f7adeaf..74cfd54 100644 --- a/lib/layout/home/home.dart +++ b/lib/layout/home/home.dart @@ -56,8 +56,8 @@ class HomePageState extends State with TickerProviderStateMixin { case AccountInfoStatus.accountLocked: return const HomeAccountLocked(); case AccountInfoStatus.accountReady: - return Provider.value( - value: accountInfo.activeAccountInfo, + return Provider.value( + value: accountInfo.activeAccountInfo!, child: BlocProvider( create: (context) => AccountRecordCubit( record: accountInfo.activeAccountInfo!.accountRecord), diff --git a/lib/layout/home/home_account_ready/chat_only.dart b/lib/layout/home/home_account_ready/chat_only.dart index 9e89b40..62d07d8 100644 --- a/lib/layout/home/home_account_ready/chat_only.dart +++ b/lib/layout/home/home_account_ready/chat_only.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import '../../../chat/chat.dart'; import '../../../tools/tools.dart'; @@ -31,10 +32,20 @@ class ChatOnlyPageState extends State super.dispose(); } + Widget buildChatComponent(BuildContext context) { + final activeChatRemoteConversationKey = + context.watch().state; + if (activeChatRemoteConversationKey == null) { + return const EmptyChatWidget(); + } + return ChatComponent.builder( + remoteConversationRecordKey: activeChatRemoteConversationKey); + } + @override Widget build(BuildContext context) => SafeArea( child: GestureDetector( onTap: () => FocusScope.of(context).requestFocus(_unfocusNode), - child: const ChatComponent(), + child: buildChatComponent(context), )); } diff --git a/lib/layout/home/home_account_ready/home_account_ready.dart b/lib/layout/home/home_account_ready/home_account_ready.dart index f49f5db..dd75d87 100644 --- a/lib/layout/home/home_account_ready/home_account_ready.dart +++ b/lib/layout/home/home_account_ready/home_account_ready.dart @@ -74,7 +74,15 @@ class HomeAccountReadyState extends State builder: (context) => Material(color: Colors.transparent, child: buildUserPanel())); - Widget buildTabletRightPane(BuildContext context) => const ChatComponent(); + Widget buildTabletRightPane(BuildContext context) { + final activeChatRemoteConversationKey = + context.watch().state; + if (activeChatRemoteConversationKey == null) { + return const EmptyChatWidget(); + } + return ChatComponent.builder( + remoteConversationRecordKey: activeChatRemoteConversationKey); + } // ignore: prefer_expression_function_bodies Widget buildTablet(BuildContext context) { @@ -106,7 +114,7 @@ class HomeAccountReadyState extends State final accountData = context.watch().state.data; if (accountData == null) { - return waitingPage(context); + return waitingPage(); } return MultiBlocProvider( diff --git a/lib/layout/home/home_account_ready/main_pager/main_pager.dart b/lib/layout/home/home_account_ready/main_pager/main_pager.dart index ee5709d..2cbee9c 100644 --- a/lib/layout/home/home_account_ready/main_pager/main_pager.dart +++ b/lib/layout/home/home_account_ready/main_pager/main_pager.dart @@ -143,7 +143,7 @@ class MainPagerState extends State with TickerProviderStateMixin { return _onNewChatBottomSheetBuilder(context); } else { // Unknown error - return waitingPage(context); + return debugPage('unknown page'); } } diff --git a/lib/layout/home/home_no_active.dart b/lib/layout/home/home_no_active.dart index 31e3378..e61fe0e 100644 --- a/lib/layout/home/home_no_active.dart +++ b/lib/layout/home/home_no_active.dart @@ -21,5 +21,5 @@ class HomeNoActiveState extends State { } @override - Widget build(BuildContext context) => waitingPage(context); + Widget build(BuildContext context) => waitingPage(); } diff --git a/lib/tools/widget_helpers.dart b/lib/tools/widget_helpers.dart index 9eddc83..c9ffa41 100644 --- a/lib/tools/widget_helpers.dart +++ b/lib/tools/widget_helpers.dart @@ -24,56 +24,72 @@ extension ModalProgressExt on Widget { return BlurryModalProgressHUD( inAsyncCall: isLoading, blurEffectIntensity: 4, - progressIndicator: buildProgressIndicator(context), + progressIndicator: buildProgressIndicator(), color: scale.tertiaryScale.appBackground.withAlpha(64), child: this); } } -Widget buildProgressIndicator(BuildContext context) { - final theme = Theme.of(context); - final scale = theme.extension()!; - return SpinKitFoldingCube( - color: scale.tertiaryScale.background, - size: 80, - ); -} +Widget buildProgressIndicator() => Builder(builder: (context) { + final theme = Theme.of(context); + final scale = theme.extension()!; + return SpinKitFoldingCube( + color: scale.tertiaryScale.background, + size: 80, + ); + }); -Widget waitingPage(BuildContext context) => ColoredBox( - color: Theme.of(context).scaffoldBackgroundColor, - child: Center(child: buildProgressIndicator(context))); +Widget waitingPage({String? text}) => Builder( + builder: (context) => ColoredBox( + color: Theme.of(context).scaffoldBackgroundColor, + child: Center( + child: Column(children: [ + buildProgressIndicator(), + if (text != null) Text(text) + ])))); -Widget errorPage(BuildContext context, Object err, StackTrace? st) => - ColoredBox( +Widget debugPage(String text) => Builder( + builder: (context) => ColoredBox( color: Theme.of(context).colorScheme.error, - child: Center(child: Text(err.toString()))); + child: Center(child: Text(text)))); + +Widget errorPage(Object err, StackTrace? st) => Builder( + builder: (context) => ColoredBox( + color: Theme.of(context).colorScheme.error, + child: Center(child: ErrorWidget(err)))); Widget asyncValueBuilder( AsyncValue av, Widget Function(BuildContext, T) builder) => av.when( - loading: () => const Builder(builder: waitingPage), - error: (e, st) => - Builder(builder: (context) => errorPage(context, e, st)), + loading: waitingPage, + error: errorPage, data: (d) => Builder(builder: (context) => builder(context, d))); extension AsyncValueBuilderExt on AsyncValue { Widget builder(Widget Function(BuildContext, T) builder) => asyncValueBuilder(this, builder); + Widget buildNotData( + {Widget Function()? loading, + Widget Function(Object, StackTrace?)? error}) => + when( + loading: () => (loading ?? waitingPage)(), + error: (e, st) => (error ?? errorPage)(e, st), + data: (d) => debugPage('AsyncValue should not be data here')); } class AsyncBlocBuilder>, S> extends BlocBuilder> { AsyncBlocBuilder({ required BlocWidgetBuilder builder, - Widget Function(BuildContext)? loading, - Widget Function(BuildContext, Object, StackTrace?)? error, + Widget Function()? loading, + Widget Function(Object, StackTrace?)? error, super.key, super.bloc, super.buildWhen, }) : super( builder: (context, state) => state.when( - loading: () => (loading ?? waitingPage)(context), - error: (e, st) => (error ?? errorPage)(context, e, st), + loading: () => (loading ?? waitingPage)(), + error: (e, st) => (error ?? errorPage)(e, st), data: (d) => builder(context, d))); } diff --git a/packages/async_tools/lib/src/single_async.dart b/packages/async_tools/lib/src/single_async.dart index aee9bc2..82334d7 100644 --- a/packages/async_tools/lib/src/single_async.dart +++ b/packages/async_tools/lib/src/single_async.dart @@ -4,8 +4,8 @@ import 'async_tag_lock.dart'; AsyncTagLock _keys = AsyncTagLock(); -void singleFuture(Object tag, Future Function() closure, - {void Function()? onBusy}) { +void singleFuture(Object tag, Future Function() closure, + {void Function()? onBusy, void Function(T)? onDone}) { if (!_keys.tryLock(tag)) { if (onBusy != null) { onBusy(); @@ -13,7 +13,13 @@ void singleFuture(Object tag, Future Function() closure, return; } unawaited(() async { - await closure(); - _keys.unlockTag(tag); + try { + final out = await closure(); + if (onDone != null) { + onDone(out); + } + } finally { + _keys.unlockTag(tag); + } }()); } From a6ba08255b999a7f5c3a24aeebfbc5311fd00f97 Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Sun, 11 Feb 2024 23:18:20 -0500 Subject: [PATCH 33/68] checkpoint --- build.bat | 7 + build.sh | 10 +- .../local_account/local_account.freezed.dart | 2 +- .../models/user_login/user_login.freezed.dart | 2 +- .../active_logins.freezed.dart | 2 +- .../new_account_page/new_account_page.dart | 2 +- lib/app.dart | 4 +- lib/chat/views/chat_component.dart | 1 - .../active_conversation_messages_cubit.dart | 2 +- .../cubits/active_conversations_cubit.dart | 9 +- lib/layout/home/home.dart | 87 +- .../home_account_ready.dart | 149 +-- ...only.dart => home_account_ready_chat.dart} | 16 +- .../home_account_ready_main.dart | 94 ++ .../home_account_ready_shell.dart | 99 ++ lib/layout/home/home_shell.dart | 42 + lib/main.dart | 6 +- lib/proto/veilidchat.pb.dart | 983 ++++++++++++------ lib/router/cubit/router_cubit.dart | 91 +- lib/router/cubit/router_cubit.freezed.dart | 193 ++++ lib/router/cubit/router_cubit.g.dart | 21 + lib/router/make_router.dart | 20 - lib/router/router.dart | 1 - lib/settings/models/preferences.freezed.dart | 2 +- .../models/theme_preference.freezed.dart | 2 +- .../processor_connection_state.freezed.dart | 2 +- macos/Podfile.lock | 26 +- packages/async_tools/build.bat | 2 + packages/async_tools/build.sh | 3 + .../lib/src/async_value.freezed.dart | 2 +- packages/async_tools/pubspec.yaml | 1 + packages/veilid_support/lib/proto/proto.dart | 22 +- pubspec.lock | 80 +- pubspec.yaml | 22 +- 34 files changed, 1301 insertions(+), 706 deletions(-) rename lib/layout/home/home_account_ready/{chat_only.dart => home_account_ready_chat.dart} (66%) create mode 100644 lib/layout/home/home_account_ready/home_account_ready_main.dart create mode 100644 lib/layout/home/home_account_ready/home_account_ready_shell.dart create mode 100644 lib/layout/home/home_shell.dart create mode 100644 lib/router/cubit/router_cubit.freezed.dart create mode 100644 lib/router/cubit/router_cubit.g.dart delete mode 100644 lib/router/make_router.dart create mode 100644 packages/async_tools/build.bat create mode 100755 packages/async_tools/build.sh diff --git a/build.bat b/build.bat index 0889dcf..e40afe0 100644 --- a/build.bat +++ b/build.bat @@ -1,6 +1,13 @@ @echo off dart run build_runner build --delete-conflicting-outputs +pushd packages\async_tools +call build.bat +popd +pushd packages\veilid_support +call build.bat +popd + pushd lib protoc --dart_out=proto -I veilid_support\proto -I veilid_support\dht_support\proto -I proto veilidchat.proto protoc --dart_out=proto -I veilid_support\proto -I veilid_support\dht_support\proto dht.proto diff --git a/build.sh b/build.sh index 569fe28..f723071 100755 --- a/build.sh +++ b/build.sh @@ -1,12 +1,16 @@ #!/bin/bash set -e +pushd packages/async_tools > /dev/null +./build.sh +popd > /dev/null + pushd packages/veilid_support > /dev/null ./build.sh popd > /dev/null dart run build_runner build --delete-conflicting-outputs -pushd lib > /dev/null -protoc --dart_out=proto -I ../packages/veilid_support/lib/proto -I ../packages/veilid_support/lib/dht_support/proto -I proto veilidchat.proto -popd > /dev/null +protoc --dart_out=lib/proto -I packages/veilid_support/lib/proto -I packages/veilid_support/lib/dht_support/proto -I lib/proto veilidchat.proto +sed -i '' 's/dht.pb.dart/package:veilid_support\/proto\/dht.pb.dart/g' lib/proto/veilidchat.pb.dart +sed -i '' 's/veilid.pb.dart/package:veilid_support\/proto\/veilid.pb.dart/g' lib/proto/veilidchat.pb.dart \ No newline at end of file diff --git a/lib/account_manager/models/local_account/local_account.freezed.dart b/lib/account_manager/models/local_account/local_account.freezed.dart index a720f3f..781d1de 100644 --- a/lib/account_manager/models/local_account/local_account.freezed.dart +++ b/lib/account_manager/models/local_account/local_account.freezed.dart @@ -12,7 +12,7 @@ part of 'local_account.dart'; T _$identity(T value) => value; final _privateConstructorUsedError = UnsupportedError( - 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#custom-getters-and-methods'); + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); LocalAccount _$LocalAccountFromJson(Map json) { return _LocalAccount.fromJson(json); diff --git a/lib/account_manager/models/user_login/user_login.freezed.dart b/lib/account_manager/models/user_login/user_login.freezed.dart index 89eb637..a25b4ab 100644 --- a/lib/account_manager/models/user_login/user_login.freezed.dart +++ b/lib/account_manager/models/user_login/user_login.freezed.dart @@ -12,7 +12,7 @@ part of 'user_login.dart'; T _$identity(T value) => value; final _privateConstructorUsedError = UnsupportedError( - 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#custom-getters-and-methods'); + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); UserLogin _$UserLoginFromJson(Map json) { return _UserLogin.fromJson(json); diff --git a/lib/account_manager/repository/account_repository/active_logins.freezed.dart b/lib/account_manager/repository/account_repository/active_logins.freezed.dart index d824640..cf9c434 100644 --- a/lib/account_manager/repository/account_repository/active_logins.freezed.dart +++ b/lib/account_manager/repository/account_repository/active_logins.freezed.dart @@ -12,7 +12,7 @@ part of 'active_logins.dart'; T _$identity(T value) => value; final _privateConstructorUsedError = UnsupportedError( - 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#custom-getters-and-methods'); + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); ActiveLogins _$ActiveLoginsFromJson(Map json) { return _ActiveLogins.fromJson(json); diff --git a/lib/account_manager/views/new_account_page/new_account_page.dart b/lib/account_manager/views/new_account_page/new_account_page.dart index fd8e54a..ab1a841 100644 --- a/lib/account_manager/views/new_account_page/new_account_page.dart +++ b/lib/account_manager/views/new_account_page/new_account_page.dart @@ -106,7 +106,7 @@ class NewAccountPageState extends State { icon: const Icon(Icons.settings), tooltip: translate('app_bar.settings_tooltip'), onPressed: () async { - context.go('/new_account/settings'); + await GoRouterHelper(context).push('/settings'); }) ]), body: _newAccountForm( diff --git a/lib/app.dart b/lib/app.dart index 7d4001d..44802bd 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -55,8 +55,8 @@ class VeilidChatApp extends StatelessWidget { child: BackgroundTicker( builder: (context) => MaterialApp.router( debugShowCheckedModeBanner: false, - routerConfig: router( - routerCubit: BlocProvider.of(context)), + routerConfig: + BlocProvider.of(context).router(), title: translate('app.title'), theme: theme, localizationsDelegates: [ diff --git a/lib/chat/views/chat_component.dart b/lib/chat/views/chat_component.dart index ac4fc81..0e3111a 100644 --- a/lib/chat/views/chat_component.dart +++ b/lib/chat/views/chat_component.dart @@ -3,7 +3,6 @@ import 'dart:async'; import 'package:async_tools/async_tools.dart'; import 'package:awesome_extensions/awesome_extensions.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; -import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_chat_types/flutter_chat_types.dart' as types; diff --git a/lib/chat_list/cubits/active_conversation_messages_cubit.dart b/lib/chat_list/cubits/active_conversation_messages_cubit.dart index 18c4c6e..032aebc 100644 --- a/lib/chat_list/cubits/active_conversation_messages_cubit.dart +++ b/lib/chat_list/cubits/active_conversation_messages_cubit.dart @@ -91,7 +91,7 @@ class ActiveConversationMessagesCubit extends BlocMapCubit add(() => MapEntry( - contact.remoteConversationRecordKey, + proto.TypedKeyProto.fromProto(contact.remoteConversationRecordKey), MessagesCubit( activeAccountInfo: _activeAccountInfo, remoteIdentityPublicKey: contact.identityPublicKey, diff --git a/lib/chat_list/cubits/active_conversations_cubit.dart b/lib/chat_list/cubits/active_conversations_cubit.dart index 0045f42..dccd9c9 100644 --- a/lib/chat_list/cubits/active_conversations_cubit.dart +++ b/lib/chat_list/cubits/active_conversations_cubit.dart @@ -40,14 +40,15 @@ class ActiveConversationsCubit extends BlocMapCubit addConversation({required proto.Contact contact}) async => add(() => MapEntry( - contact.remoteConversationRecordKey, + contact.remoteConversationRecordKey.toVeilid(), TransformerCubit( ConversationCubit( activeAccountInfo: _activeAccountInfo, - remoteIdentityPublicKey: contact.identityPublicKey, - localConversationRecordKey: contact.localConversationRecordKey, + remoteIdentityPublicKey: contact.identityPublicKey.toVeilid(), + localConversationRecordKey: + contact.localConversationRecordKey.toVeilid(), remoteConversationRecordKey: - contact.remoteConversationRecordKey, + contact.remoteConversationRecordKey.toVeilid(), ), // Transformer that only passes through completed conversations // along with the contact that corresponds to the completed diff --git a/lib/layout/home/home.dart b/lib/layout/home/home.dart index 74cfd54..5b1b3d1 100644 --- a/lib/layout/home/home.dart +++ b/lib/layout/home/home.dart @@ -1,81 +1,6 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:provider/provider.dart'; - -import '../../account_manager/account_manager.dart'; -import '../../theme/theme.dart'; -import '../../tools/tools.dart'; -import 'home_account_invalid.dart'; -import 'home_account_locked.dart'; -import 'home_account_missing.dart'; -import 'home_account_ready/home_account_ready.dart'; -import 'home_no_active.dart'; - -class HomePage extends StatefulWidget { - const HomePage({super.key}); - - @override - HomePageState createState() => HomePageState(); -} - -class HomePageState extends State with TickerProviderStateMixin { - final _unfocusNode = FocusNode(); - - @override - void initState() { - super.initState(); - - WidgetsBinding.instance.addPostFrameCallback((_) async { - await changeWindowSetup( - TitleBarStyle.normal, OrientationCapability.normal); - }); - } - - @override - void dispose() { - _unfocusNode.dispose(); - super.dispose(); - } - - Widget buildWithLogin(BuildContext context) { - final activeUserLogin = context.watch().state; - - if (activeUserLogin == null) { - // If no logged in user is active, show the loading panel - return const HomeNoActive(); - } - - final accountInfo = AccountRepository.instance - .getAccountInfo(accountMasterRecordKey: activeUserLogin)!; - - switch (accountInfo.status) { - case AccountInfoStatus.noAccount: - return const HomeAccountMissing(); - case AccountInfoStatus.accountInvalid: - return const HomeAccountInvalid(); - case AccountInfoStatus.accountLocked: - return const HomeAccountLocked(); - case AccountInfoStatus.accountReady: - return Provider.value( - value: accountInfo.activeAccountInfo!, - child: BlocProvider( - create: (context) => AccountRecordCubit( - record: accountInfo.activeAccountInfo!.accountRecord), - child: const HomeAccountReady())); - } - } - - @override - Widget build(BuildContext context) { - final theme = Theme.of(context); - final scale = theme.extension()!; - - return SafeArea( - child: GestureDetector( - onTap: () => FocusScope.of(context).requestFocus(_unfocusNode), - child: DecoratedBox( - decoration: BoxDecoration( - color: scale.primaryScale.activeElementBackground), - child: buildWithLogin(context)))); - } -} +export 'home_account_invalid.dart'; +export 'home_account_locked.dart'; +export 'home_account_missing.dart'; +export 'home_account_ready/home_account_ready.dart'; +export 'home_no_active.dart'; +export 'home_shell.dart'; diff --git a/lib/layout/home/home_account_ready/home_account_ready.dart b/lib/layout/home/home_account_ready/home_account_ready.dart index dd75d87..b198f0b 100644 --- a/lib/layout/home/home_account_ready/home_account_ready.dart +++ b/lib/layout/home/home_account_ready/home_account_ready.dart @@ -1,146 +1,3 @@ -import 'package:awesome_extensions/awesome_extensions.dart'; -import 'package:fast_immutable_collections/fast_immutable_collections.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:flutter_translate/flutter_translate.dart'; -import 'package:go_router/go_router.dart'; - -import '../../../account_manager/account_manager.dart'; -import '../../../chat/chat.dart'; -import '../../../chat_list/chat_list.dart'; -import '../../../contact_invitation/contact_invitation.dart'; -import '../../../contacts/contacts.dart'; -import '../../../theme/theme.dart'; -import '../../../tools/tools.dart'; -import 'main_pager/main_pager.dart'; - -class HomeAccountReady extends StatefulWidget { - const HomeAccountReady({super.key}); - - @override - HomeAccountReadyState createState() => HomeAccountReadyState(); -} - -class HomeAccountReadyState extends State - with TickerProviderStateMixin { - // - @override - void initState() { - super.initState(); - } - - Widget buildUnlockAccount( - BuildContext context, - IList localAccounts, - // ignore: prefer_expression_function_bodies - ) { - return const Center(child: Text('unlock account')); - } - - Widget buildUserPanel() => Builder(builder: (context) { - final account = context.watch().state; - final theme = Theme.of(context); - final scale = theme.extension()!; - - return Column(children: [ - Row(children: [ - IconButton( - icon: const Icon(Icons.settings), - color: scale.secondaryScale.text, - constraints: const BoxConstraints.expand(height: 64, width: 64), - style: ButtonStyle( - backgroundColor: - MaterialStateProperty.all(scale.secondaryScale.border), - shape: MaterialStateProperty.all( - const RoundedRectangleBorder( - borderRadius: - BorderRadius.all(Radius.circular(16))))), - tooltip: translate('app_bar.settings_tooltip'), - onPressed: () async { - context.go('/home/settings'); - }).paddingLTRB(0, 0, 8, 0), - asyncValueBuilder(account, - (_, account) => ProfileWidget(profile: account.profile)) - .expanded(), - ]).paddingAll(8), - const MainPager().expanded() - ]); - }); - - Widget buildPhone(BuildContext context) => - Material(color: Colors.transparent, child: buildUserPanel()); - - Widget buildTabletLeftPane(BuildContext context) => Builder( - builder: (context) => - Material(color: Colors.transparent, child: buildUserPanel())); - - Widget buildTabletRightPane(BuildContext context) { - final activeChatRemoteConversationKey = - context.watch().state; - if (activeChatRemoteConversationKey == null) { - return const EmptyChatWidget(); - } - return ChatComponent.builder( - remoteConversationRecordKey: activeChatRemoteConversationKey); - } - - // ignore: prefer_expression_function_bodies - Widget buildTablet(BuildContext context) { - final w = MediaQuery.of(context).size.width; - final theme = Theme.of(context); - final scale = theme.extension()!; - - final children = [ - ConstrainedBox( - constraints: const BoxConstraints(minWidth: 300, maxWidth: 300), - child: ConstrainedBox( - constraints: BoxConstraints(maxWidth: w / 2), - child: buildTabletLeftPane(context))), - SizedBox( - width: 2, - height: double.infinity, - child: ColoredBox(color: scale.primaryScale.hoverBorder)), - Expanded(child: buildTabletRightPane(context)), - ]; - - return Row( - children: children, - ); - } - - @override - Widget build(BuildContext context) { - final activeAccountInfo = context.watch(); - final accountData = context.watch().state.data; - - if (accountData == null) { - return waitingPage(); - } - - return MultiBlocProvider( - providers: [ - BlocProvider( - create: (context) => ContactInvitationListCubit( - activeAccountInfo: activeAccountInfo, - account: accountData.value)), - BlocProvider( - create: (context) => ContactListCubit( - activeAccountInfo: activeAccountInfo, - account: accountData.value)), - BlocProvider( - create: (context) => ChatListCubit( - activeAccountInfo: activeAccountInfo, - account: accountData.value)), - BlocProvider( - create: (context) => ActiveConversationsCubit( - activeAccountInfo: activeAccountInfo)), - BlocProvider(create: (context) => ActiveChatCubit(null)) - ], - child: responsiveVisibility( - context: context, - phone: false, - ) - ? buildTablet(context) - : buildPhone(context)); - } -} +export 'home_account_ready_chat.dart'; +export 'home_account_ready_main.dart'; +export 'home_account_ready_shell.dart'; diff --git a/lib/layout/home/home_account_ready/chat_only.dart b/lib/layout/home/home_account_ready/home_account_ready_chat.dart similarity index 66% rename from lib/layout/home/home_account_ready/chat_only.dart rename to lib/layout/home/home_account_ready/home_account_ready_chat.dart index 62d07d8..d046b02 100644 --- a/lib/layout/home/home_account_ready/chat_only.dart +++ b/lib/layout/home/home_account_ready/home_account_ready_chat.dart @@ -2,28 +2,20 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import '../../../chat/chat.dart'; -import '../../../tools/tools.dart'; -class ChatOnlyPage extends StatefulWidget { - const ChatOnlyPage({super.key}); +class HomeAccountReadyChat extends StatefulWidget { + const HomeAccountReadyChat({super.key}); @override - ChatOnlyPageState createState() => ChatOnlyPageState(); + HomeAccountReadyChatState createState() => HomeAccountReadyChatState(); } -class ChatOnlyPageState extends State - with TickerProviderStateMixin { +class HomeAccountReadyChatState extends State { final _unfocusNode = FocusNode(); @override void initState() { super.initState(); - - WidgetsBinding.instance.addPostFrameCallback((_) async { - setState(() {}); - await changeWindowSetup( - TitleBarStyle.normal, OrientationCapability.normal); - }); } @override diff --git a/lib/layout/home/home_account_ready/home_account_ready_main.dart b/lib/layout/home/home_account_ready/home_account_ready_main.dart new file mode 100644 index 0000000..2a5ee52 --- /dev/null +++ b/lib/layout/home/home_account_ready/home_account_ready_main.dart @@ -0,0 +1,94 @@ +import 'package:awesome_extensions/awesome_extensions.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_translate/flutter_translate.dart'; +import 'package:go_router/go_router.dart'; + +import '../../../account_manager/account_manager.dart'; +import '../../../chat/chat.dart'; +import '../../../theme/theme.dart'; +import '../../../tools/tools.dart'; +import 'main_pager/main_pager.dart'; + +class HomeAccountReadyMain extends StatelessWidget { + const HomeAccountReadyMain({super.key}); + + Widget buildUserPanel() => Builder(builder: (context) { + final account = context.watch().state; + final theme = Theme.of(context); + final scale = theme.extension()!; + + return Column(children: [ + Row(children: [ + IconButton( + icon: const Icon(Icons.settings), + color: scale.secondaryScale.text, + constraints: const BoxConstraints.expand(height: 64, width: 64), + style: ButtonStyle( + backgroundColor: + MaterialStateProperty.all(scale.secondaryScale.border), + shape: MaterialStateProperty.all( + const RoundedRectangleBorder( + borderRadius: + BorderRadius.all(Radius.circular(16))))), + tooltip: translate('app_bar.settings_tooltip'), + onPressed: () async { + await GoRouterHelper(context).push('/settings'); + }).paddingLTRB(0, 0, 8, 0), + asyncValueBuilder(account, + (_, account) => ProfileWidget(profile: account.profile)) + .expanded(), + ]).paddingAll(8), + const MainPager().expanded() + ]); + }); + + Widget buildPhone(BuildContext context) => + Material(color: Colors.transparent, child: buildUserPanel()); + + Widget buildTabletLeftPane(BuildContext context) => Builder( + builder: (context) => + Material(color: Colors.transparent, child: buildUserPanel())); + + Widget buildTabletRightPane(BuildContext context) { + final activeChatRemoteConversationKey = + context.watch().state; + if (activeChatRemoteConversationKey == null) { + return const EmptyChatWidget(); + } + return ChatComponent.builder( + remoteConversationRecordKey: activeChatRemoteConversationKey); + } + + // ignore: prefer_expression_function_bodies + Widget buildTablet(BuildContext context) { + final w = MediaQuery.of(context).size.width; + final theme = Theme.of(context); + final scale = theme.extension()!; + + final children = [ + ConstrainedBox( + constraints: const BoxConstraints(minWidth: 300, maxWidth: 300), + child: ConstrainedBox( + constraints: BoxConstraints(maxWidth: w / 2), + child: buildTabletLeftPane(context))), + SizedBox( + width: 2, + height: double.infinity, + child: ColoredBox(color: scale.primaryScale.hoverBorder)), + Expanded(child: buildTabletRightPane(context)), + ]; + + return Row( + children: children, + ); + } + + @override + Widget build(BuildContext context) => responsiveVisibility( + context: context, + phone: false, + ) + ? buildTablet(context) + : buildPhone(context); +} diff --git a/lib/layout/home/home_account_ready/home_account_ready_shell.dart b/lib/layout/home/home_account_ready/home_account_ready_shell.dart new file mode 100644 index 0000000..22e1266 --- /dev/null +++ b/lib/layout/home/home_account_ready/home_account_ready_shell.dart @@ -0,0 +1,99 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:provider/provider.dart'; + +import '../../../account_manager/account_manager.dart'; +import '../../../chat/chat.dart'; +import '../../../chat_list/chat_list.dart'; +import '../../../contact_invitation/contact_invitation.dart'; +import '../../../contacts/contacts.dart'; +import '../../../tools/tools.dart'; + +class HomeAccountReadyShell extends StatefulWidget { + const HomeAccountReadyShell({required this.child, super.key}); + + @override + HomeAccountReadyShellState createState() => HomeAccountReadyShellState(); + + final Widget child; +} + +class HomeAccountReadyShellState extends State + with TickerProviderStateMixin { + // + @override + void initState() { + super.initState(); + } + + // xxx figure out how to do this switch + + // Widget buildWithLogin(BuildContext context) { + // final activeUserLogin = context.watch().state; + + // if (activeUserLogin == null) { + // // If no logged in user is active, show the loading panel + // return const HomeNoActive(); + // } + + // final accountInfo = AccountRepository.instance + // .getAccountInfo(accountMasterRecordKey: activeUserLogin)!; + + // switch (accountInfo.status) { + // case AccountInfoStatus.noAccount: + // return const HomeAccountMissing(); + // case AccountInfoStatus.accountInvalid: + // return const HomeAccountInvalid(); + // case AccountInfoStatus.accountLocked: + // return const HomeAccountLocked(); + // case AccountInfoStatus.accountReady: + // return Provider.value( + // value: accountInfo.activeAccountInfo!, + // child: BlocProvider( + // create: (context) => AccountRecordCubit( + // record: accountInfo.activeAccountInfo!.accountRecord), + // child: const HomeAccountReady())); + // } + // } + + @override + Widget build(BuildContext context) { + // These must be valid already before making this widget, + // per the ShellRoute above it + final activeUserLogin = context.read().state!; + final accountInfo = AccountRepository.instance + .getAccountInfo(accountMasterRecordKey: activeUserLogin)!; + final activeAccountInfo = accountInfo.activeAccountInfo!; + + return Provider.value( + value: activeAccountInfo, + child: BlocProvider( + create: (context) => AccountRecordCubit( + record: accountInfo.activeAccountInfo!.accountRecord), + child: Builder(builder: (context) { + final account = + context.watch().state.data?.value; + if (account == null) { + return waitingPage(); + } + return MultiBlocProvider(providers: [ + BlocProvider( + create: (context) => ContactInvitationListCubit( + activeAccountInfo: activeAccountInfo, + account: account)), + BlocProvider( + create: (context) => ContactListCubit( + activeAccountInfo: activeAccountInfo, + account: account)), + BlocProvider( + create: (context) => ChatListCubit( + activeAccountInfo: activeAccountInfo, + account: account)), + BlocProvider( + create: (context) => ActiveConversationsCubit( + activeAccountInfo: activeAccountInfo)), + BlocProvider(create: (context) => ActiveChatCubit(null)) + ], child: widget.child); + }))); + } +} diff --git a/lib/layout/home/home_shell.dart b/lib/layout/home/home_shell.dart new file mode 100644 index 0000000..1124def --- /dev/null +++ b/lib/layout/home/home_shell.dart @@ -0,0 +1,42 @@ +import 'package:flutter/material.dart'; + +import '../../theme/theme.dart'; + +class HomeShell extends StatefulWidget { + const HomeShell({required this.child, super.key}); + + @override + HomeShellState createState() => HomeShellState(); + + final Widget child; +} + +class HomeShellState extends State { + final _unfocusNode = FocusNode(); + + @override + void initState() { + super.initState(); + } + + @override + void dispose() { + _unfocusNode.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final scale = theme.extension()!; + + // XXX: eventually write account switcher here + return SafeArea( + child: GestureDetector( + onTap: () => FocusScope.of(context).requestFocus(_unfocusNode), + child: DecoratedBox( + decoration: BoxDecoration( + color: scale.primaryScale.activeElementBackground), + child: widget.child))); + } +} diff --git a/lib/main.dart b/lib/main.dart index 0977885..49c3cb7 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -42,7 +42,7 @@ void main() async { await initializeWindowControl(); // Make localization delegate - final delegate = await LocalizationDelegate.create( + final localizationDelegate = await LocalizationDelegate.create( fallbackLocale: 'en_US', supportedLocales: ['en_US']); await initializeDateFormatting(); @@ -51,8 +51,8 @@ void main() async { // Run the app // Hot reloads will only restart this part, not Veilid - runApp(LocalizedApp( - delegate, VeilidChatApp(initialThemeData: initialThemeData))); + runApp(LocalizedApp(localizationDelegate, + VeilidChatApp(initialThemeData: initialThemeData))); }, (error, stackTrace) { log.error('Dart Runtime: {$error}\n{$stackTrace}'); }); diff --git a/lib/proto/veilidchat.pb.dart b/lib/proto/veilidchat.pb.dart index 6628b46..af40e07 100644 --- a/lib/proto/veilidchat.pb.dart +++ b/lib/proto/veilidchat.pb.dart @@ -14,8 +14,8 @@ import 'dart:core' as $core; import 'package:fixnum/fixnum.dart' as $fixnum; import 'package:protobuf/protobuf.dart' as $pb; -import 'dht.pb.dart' as $0; -import 'veilid.pb.dart' as $1; +import 'package:veilid_support/proto/dht.pb.dart' as $0; +import 'package:veilid_support/proto/veilid.pb.dart' as $1; import 'veilidchat.pbenum.dart'; export 'veilidchat.pbenum.dart'; @@ -23,28 +23,38 @@ export 'veilidchat.pbenum.dart'; class Attachment extends $pb.GeneratedMessage { factory Attachment() => create(); Attachment._() : super(); - factory Attachment.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); - factory Attachment.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + factory Attachment.fromBuffer($core.List<$core.int> i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(i, r); + factory Attachment.fromJson($core.String i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(i, r); - static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'Attachment', package: const $pb.PackageName(_omitMessageNames ? '' : 'veilidchat'), createEmptyInstance: create) - ..e(1, _omitFieldNames ? '' : 'kind', $pb.PbFieldType.OE, defaultOrMaker: AttachmentKind.ATTACHMENT_KIND_UNSPECIFIED, valueOf: AttachmentKind.valueOf, enumValues: AttachmentKind.values) + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames ? '' : 'Attachment', + package: const $pb.PackageName(_omitMessageNames ? '' : 'veilidchat'), + createEmptyInstance: create) + ..e(1, _omitFieldNames ? '' : 'kind', $pb.PbFieldType.OE, + defaultOrMaker: AttachmentKind.ATTACHMENT_KIND_UNSPECIFIED, + valueOf: AttachmentKind.valueOf, + enumValues: AttachmentKind.values) ..aOS(2, _omitFieldNames ? '' : 'mime') ..aOS(3, _omitFieldNames ? '' : 'name') - ..aOM<$0.DataReference>(4, _omitFieldNames ? '' : 'content', subBuilder: $0.DataReference.create) - ..aOM<$1.Signature>(5, _omitFieldNames ? '' : 'signature', subBuilder: $1.Signature.create) - ..hasRequiredFields = false - ; + ..aOM<$0.DataReference>(4, _omitFieldNames ? '' : 'content', + subBuilder: $0.DataReference.create) + ..aOM<$1.Signature>(5, _omitFieldNames ? '' : 'signature', + subBuilder: $1.Signature.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') + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') Attachment clone() => Attachment()..mergeFromMessage(this); - @$core.Deprecated( - 'Using this can add significant overhead to your binary. ' - 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' - 'Will be removed in next major version') - Attachment copyWith(void Function(Attachment) updates) => super.copyWith((message) => updates(message as Attachment)) as Attachment; + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + Attachment copyWith(void Function(Attachment) updates) => + super.copyWith((message) => updates(message as Attachment)) as Attachment; $pb.BuilderInfo get info_ => _i; @@ -53,13 +63,17 @@ class Attachment extends $pb.GeneratedMessage { Attachment createEmptyInstance() => create(); static $pb.PbList createRepeated() => $pb.PbList(); @$core.pragma('dart2js:noInline') - static Attachment getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static Attachment getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor(create); static Attachment? _defaultInstance; @$pb.TagNumber(1) AttachmentKind get kind => $_getN(0); @$pb.TagNumber(1) - set kind(AttachmentKind v) { setField(1, v); } + set kind(AttachmentKind v) { + setField(1, v); + } + @$pb.TagNumber(1) $core.bool hasKind() => $_has(0); @$pb.TagNumber(1) @@ -68,7 +82,10 @@ class Attachment extends $pb.GeneratedMessage { @$pb.TagNumber(2) $core.String get mime => $_getSZ(1); @$pb.TagNumber(2) - set mime($core.String v) { $_setString(1, v); } + set mime($core.String v) { + $_setString(1, v); + } + @$pb.TagNumber(2) $core.bool hasMime() => $_has(1); @$pb.TagNumber(2) @@ -77,7 +94,10 @@ class Attachment extends $pb.GeneratedMessage { @$pb.TagNumber(3) $core.String get name => $_getSZ(2); @$pb.TagNumber(3) - set name($core.String v) { $_setString(2, v); } + set name($core.String v) { + $_setString(2, v); + } + @$pb.TagNumber(3) $core.bool hasName() => $_has(2); @$pb.TagNumber(3) @@ -86,7 +106,10 @@ class Attachment extends $pb.GeneratedMessage { @$pb.TagNumber(4) $0.DataReference get content => $_getN(3); @$pb.TagNumber(4) - set content($0.DataReference v) { setField(4, v); } + set content($0.DataReference v) { + setField(4, v); + } + @$pb.TagNumber(4) $core.bool hasContent() => $_has(3); @$pb.TagNumber(4) @@ -97,7 +120,10 @@ class Attachment extends $pb.GeneratedMessage { @$pb.TagNumber(5) $1.Signature get signature => $_getN(4); @$pb.TagNumber(5) - set signature($1.Signature v) { setField(5, v); } + set signature($1.Signature v) { + setField(5, v); + } + @$pb.TagNumber(5) $core.bool hasSignature() => $_has(4); @$pb.TagNumber(5) @@ -109,28 +135,39 @@ class Attachment extends $pb.GeneratedMessage { class Message extends $pb.GeneratedMessage { factory Message() => create(); Message._() : super(); - factory Message.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); - factory Message.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + factory Message.fromBuffer($core.List<$core.int> i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(i, r); + factory Message.fromJson($core.String i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(i, r); - static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'Message', package: const $pb.PackageName(_omitMessageNames ? '' : 'veilidchat'), createEmptyInstance: create) - ..aOM<$1.TypedKey>(1, _omitFieldNames ? '' : 'author', subBuilder: $1.TypedKey.create) - ..a<$fixnum.Int64>(2, _omitFieldNames ? '' : 'timestamp', $pb.PbFieldType.OU6, defaultOrMaker: $fixnum.Int64.ZERO) + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames ? '' : 'Message', + package: const $pb.PackageName(_omitMessageNames ? '' : 'veilidchat'), + createEmptyInstance: create) + ..aOM<$1.TypedKey>(1, _omitFieldNames ? '' : 'author', + subBuilder: $1.TypedKey.create) + ..a<$fixnum.Int64>( + 2, _omitFieldNames ? '' : 'timestamp', $pb.PbFieldType.OU6, + defaultOrMaker: $fixnum.Int64.ZERO) ..aOS(3, _omitFieldNames ? '' : 'text') - ..aOM<$1.Signature>(4, _omitFieldNames ? '' : 'signature', subBuilder: $1.Signature.create) - ..pc(5, _omitFieldNames ? '' : 'attachments', $pb.PbFieldType.PM, subBuilder: Attachment.create) - ..hasRequiredFields = false - ; + ..aOM<$1.Signature>(4, _omitFieldNames ? '' : 'signature', + subBuilder: $1.Signature.create) + ..pc( + 5, _omitFieldNames ? '' : 'attachments', $pb.PbFieldType.PM, + subBuilder: Attachment.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') + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') Message clone() => Message()..mergeFromMessage(this); - @$core.Deprecated( - 'Using this can add significant overhead to your binary. ' - 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' - 'Will be removed in next major version') - Message copyWith(void Function(Message) updates) => super.copyWith((message) => updates(message as Message)) as Message; + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + Message copyWith(void Function(Message) updates) => + super.copyWith((message) => updates(message as Message)) as Message; $pb.BuilderInfo get info_ => _i; @@ -139,13 +176,17 @@ class Message extends $pb.GeneratedMessage { Message createEmptyInstance() => create(); static $pb.PbList createRepeated() => $pb.PbList(); @$core.pragma('dart2js:noInline') - static Message getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static Message getDefault() => + _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); static Message? _defaultInstance; @$pb.TagNumber(1) $1.TypedKey get author => $_getN(0); @$pb.TagNumber(1) - set author($1.TypedKey v) { setField(1, v); } + set author($1.TypedKey v) { + setField(1, v); + } + @$pb.TagNumber(1) $core.bool hasAuthor() => $_has(0); @$pb.TagNumber(1) @@ -156,7 +197,10 @@ class Message extends $pb.GeneratedMessage { @$pb.TagNumber(2) $fixnum.Int64 get timestamp => $_getI64(1); @$pb.TagNumber(2) - set timestamp($fixnum.Int64 v) { $_setInt64(1, v); } + set timestamp($fixnum.Int64 v) { + $_setInt64(1, v); + } + @$pb.TagNumber(2) $core.bool hasTimestamp() => $_has(1); @$pb.TagNumber(2) @@ -165,7 +209,10 @@ class Message extends $pb.GeneratedMessage { @$pb.TagNumber(3) $core.String get text => $_getSZ(2); @$pb.TagNumber(3) - set text($core.String v) { $_setString(2, v); } + set text($core.String v) { + $_setString(2, v); + } + @$pb.TagNumber(3) $core.bool hasText() => $_has(2); @$pb.TagNumber(3) @@ -174,7 +221,10 @@ class Message extends $pb.GeneratedMessage { @$pb.TagNumber(4) $1.Signature get signature => $_getN(3); @$pb.TagNumber(4) - set signature($1.Signature v) { setField(4, v); } + set signature($1.Signature v) { + setField(4, v); + } + @$pb.TagNumber(4) $core.bool hasSignature() => $_has(3); @$pb.TagNumber(4) @@ -189,41 +239,54 @@ class Message extends $pb.GeneratedMessage { class Conversation extends $pb.GeneratedMessage { factory Conversation() => create(); Conversation._() : super(); - factory Conversation.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); - factory Conversation.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + factory Conversation.fromBuffer($core.List<$core.int> i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(i, r); + factory Conversation.fromJson($core.String i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(i, r); - static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'Conversation', package: const $pb.PackageName(_omitMessageNames ? '' : 'veilidchat'), createEmptyInstance: create) - ..aOM(1, _omitFieldNames ? '' : 'profile', subBuilder: Profile.create) + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames ? '' : 'Conversation', + package: const $pb.PackageName(_omitMessageNames ? '' : 'veilidchat'), + createEmptyInstance: create) + ..aOM(1, _omitFieldNames ? '' : 'profile', + subBuilder: Profile.create) ..aOS(2, _omitFieldNames ? '' : 'identityMasterJson') - ..aOM<$1.TypedKey>(3, _omitFieldNames ? '' : 'messages', subBuilder: $1.TypedKey.create) - ..hasRequiredFields = false - ; + ..aOM<$1.TypedKey>(3, _omitFieldNames ? '' : 'messages', + subBuilder: $1.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') + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') Conversation clone() => Conversation()..mergeFromMessage(this); - @$core.Deprecated( - 'Using this can add significant overhead to your binary. ' - 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' - 'Will be removed in next major version') - Conversation copyWith(void Function(Conversation) updates) => super.copyWith((message) => updates(message as Conversation)) as Conversation; + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + Conversation copyWith(void Function(Conversation) updates) => + super.copyWith((message) => updates(message as Conversation)) + as Conversation; $pb.BuilderInfo get info_ => _i; @$core.pragma('dart2js:noInline') static Conversation create() => Conversation._(); Conversation createEmptyInstance() => create(); - static $pb.PbList createRepeated() => $pb.PbList(); + static $pb.PbList createRepeated() => + $pb.PbList(); @$core.pragma('dart2js:noInline') - static Conversation getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static Conversation getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor(create); static Conversation? _defaultInstance; @$pb.TagNumber(1) Profile get profile => $_getN(0); @$pb.TagNumber(1) - set profile(Profile v) { setField(1, v); } + set profile(Profile v) { + setField(1, v); + } + @$pb.TagNumber(1) $core.bool hasProfile() => $_has(0); @$pb.TagNumber(1) @@ -234,7 +297,10 @@ class Conversation extends $pb.GeneratedMessage { @$pb.TagNumber(2) $core.String get identityMasterJson => $_getSZ(1); @$pb.TagNumber(2) - set identityMasterJson($core.String v) { $_setString(1, v); } + set identityMasterJson($core.String v) { + $_setString(1, v); + } + @$pb.TagNumber(2) $core.bool hasIdentityMasterJson() => $_has(1); @$pb.TagNumber(2) @@ -243,7 +309,10 @@ class Conversation extends $pb.GeneratedMessage { @$pb.TagNumber(3) $1.TypedKey get messages => $_getN(2); @$pb.TagNumber(3) - set messages($1.TypedKey v) { setField(3, v); } + set messages($1.TypedKey v) { + setField(3, v); + } + @$pb.TagNumber(3) $core.bool hasMessages() => $_has(2); @$pb.TagNumber(3) @@ -255,30 +324,40 @@ class Conversation extends $pb.GeneratedMessage { class Contact extends $pb.GeneratedMessage { factory Contact() => create(); Contact._() : super(); - factory Contact.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); - factory Contact.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + factory Contact.fromBuffer($core.List<$core.int> i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(i, r); + factory Contact.fromJson($core.String i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(i, r); - static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'Contact', package: const $pb.PackageName(_omitMessageNames ? '' : 'veilidchat'), createEmptyInstance: create) - ..aOM(1, _omitFieldNames ? '' : 'editedProfile', subBuilder: Profile.create) - ..aOM(2, _omitFieldNames ? '' : 'remoteProfile', subBuilder: Profile.create) + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames ? '' : 'Contact', + package: const $pb.PackageName(_omitMessageNames ? '' : 'veilidchat'), + createEmptyInstance: create) + ..aOM(1, _omitFieldNames ? '' : 'editedProfile', + subBuilder: Profile.create) + ..aOM(2, _omitFieldNames ? '' : 'remoteProfile', + subBuilder: Profile.create) ..aOS(3, _omitFieldNames ? '' : 'identityMasterJson') - ..aOM<$1.TypedKey>(4, _omitFieldNames ? '' : 'identityPublicKey', subBuilder: $1.TypedKey.create) - ..aOM<$1.TypedKey>(5, _omitFieldNames ? '' : 'remoteConversationRecordKey', subBuilder: $1.TypedKey.create) - ..aOM<$1.TypedKey>(6, _omitFieldNames ? '' : 'localConversationRecordKey', subBuilder: $1.TypedKey.create) + ..aOM<$1.TypedKey>(4, _omitFieldNames ? '' : 'identityPublicKey', + subBuilder: $1.TypedKey.create) + ..aOM<$1.TypedKey>(5, _omitFieldNames ? '' : 'remoteConversationRecordKey', + subBuilder: $1.TypedKey.create) + ..aOM<$1.TypedKey>(6, _omitFieldNames ? '' : 'localConversationRecordKey', + subBuilder: $1.TypedKey.create) ..aOB(7, _omitFieldNames ? '' : 'showAvailability') - ..hasRequiredFields = false - ; + ..hasRequiredFields = false; - @$core.Deprecated( - 'Using this can add significant overhead to your binary. ' - 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' - 'Will be removed in next major version') + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') Contact clone() => Contact()..mergeFromMessage(this); - @$core.Deprecated( - 'Using this can add significant overhead to your binary. ' - 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' - 'Will be removed in next major version') - Contact copyWith(void Function(Contact) updates) => super.copyWith((message) => updates(message as Contact)) as Contact; + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + Contact copyWith(void Function(Contact) updates) => + super.copyWith((message) => updates(message as Contact)) as Contact; $pb.BuilderInfo get info_ => _i; @@ -287,13 +366,17 @@ class Contact extends $pb.GeneratedMessage { Contact createEmptyInstance() => create(); static $pb.PbList createRepeated() => $pb.PbList(); @$core.pragma('dart2js:noInline') - static Contact getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static Contact getDefault() => + _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); static Contact? _defaultInstance; @$pb.TagNumber(1) Profile get editedProfile => $_getN(0); @$pb.TagNumber(1) - set editedProfile(Profile v) { setField(1, v); } + set editedProfile(Profile v) { + setField(1, v); + } + @$pb.TagNumber(1) $core.bool hasEditedProfile() => $_has(0); @$pb.TagNumber(1) @@ -304,7 +387,10 @@ class Contact extends $pb.GeneratedMessage { @$pb.TagNumber(2) Profile get remoteProfile => $_getN(1); @$pb.TagNumber(2) - set remoteProfile(Profile v) { setField(2, v); } + set remoteProfile(Profile v) { + setField(2, v); + } + @$pb.TagNumber(2) $core.bool hasRemoteProfile() => $_has(1); @$pb.TagNumber(2) @@ -315,7 +401,10 @@ class Contact extends $pb.GeneratedMessage { @$pb.TagNumber(3) $core.String get identityMasterJson => $_getSZ(2); @$pb.TagNumber(3) - set identityMasterJson($core.String v) { $_setString(2, v); } + set identityMasterJson($core.String v) { + $_setString(2, v); + } + @$pb.TagNumber(3) $core.bool hasIdentityMasterJson() => $_has(2); @$pb.TagNumber(3) @@ -324,7 +413,10 @@ class Contact extends $pb.GeneratedMessage { @$pb.TagNumber(4) $1.TypedKey get identityPublicKey => $_getN(3); @$pb.TagNumber(4) - set identityPublicKey($1.TypedKey v) { setField(4, v); } + set identityPublicKey($1.TypedKey v) { + setField(4, v); + } + @$pb.TagNumber(4) $core.bool hasIdentityPublicKey() => $_has(3); @$pb.TagNumber(4) @@ -335,7 +427,10 @@ class Contact extends $pb.GeneratedMessage { @$pb.TagNumber(5) $1.TypedKey get remoteConversationRecordKey => $_getN(4); @$pb.TagNumber(5) - set remoteConversationRecordKey($1.TypedKey v) { setField(5, v); } + set remoteConversationRecordKey($1.TypedKey v) { + setField(5, v); + } + @$pb.TagNumber(5) $core.bool hasRemoteConversationRecordKey() => $_has(4); @$pb.TagNumber(5) @@ -346,7 +441,10 @@ class Contact extends $pb.GeneratedMessage { @$pb.TagNumber(6) $1.TypedKey get localConversationRecordKey => $_getN(5); @$pb.TagNumber(6) - set localConversationRecordKey($1.TypedKey v) { setField(6, v); } + set localConversationRecordKey($1.TypedKey v) { + setField(6, v); + } + @$pb.TagNumber(6) $core.bool hasLocalConversationRecordKey() => $_has(5); @$pb.TagNumber(6) @@ -357,7 +455,10 @@ class Contact extends $pb.GeneratedMessage { @$pb.TagNumber(7) $core.bool get showAvailability => $_getBF(6); @$pb.TagNumber(7) - set showAvailability($core.bool v) { $_setBool(6, v); } + set showAvailability($core.bool v) { + $_setBool(6, v); + } + @$pb.TagNumber(7) $core.bool hasShowAvailability() => $_has(6); @$pb.TagNumber(7) @@ -367,28 +468,38 @@ class Contact extends $pb.GeneratedMessage { class Profile extends $pb.GeneratedMessage { factory Profile() => create(); Profile._() : super(); - factory Profile.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); - factory Profile.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + factory Profile.fromBuffer($core.List<$core.int> i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(i, r); + factory Profile.fromJson($core.String i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(i, r); - static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'Profile', package: const $pb.PackageName(_omitMessageNames ? '' : 'veilidchat'), createEmptyInstance: create) + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames ? '' : 'Profile', + package: const $pb.PackageName(_omitMessageNames ? '' : 'veilidchat'), + createEmptyInstance: create) ..aOS(1, _omitFieldNames ? '' : 'name') ..aOS(2, _omitFieldNames ? '' : 'pronouns') ..aOS(3, _omitFieldNames ? '' : 'status') - ..e(4, _omitFieldNames ? '' : 'availability', $pb.PbFieldType.OE, defaultOrMaker: Availability.AVAILABILITY_UNSPECIFIED, valueOf: Availability.valueOf, enumValues: Availability.values) - ..aOM<$1.TypedKey>(5, _omitFieldNames ? '' : 'avatar', subBuilder: $1.TypedKey.create) - ..hasRequiredFields = false - ; + ..e( + 4, _omitFieldNames ? '' : 'availability', $pb.PbFieldType.OE, + defaultOrMaker: Availability.AVAILABILITY_UNSPECIFIED, + valueOf: Availability.valueOf, + enumValues: Availability.values) + ..aOM<$1.TypedKey>(5, _omitFieldNames ? '' : 'avatar', + subBuilder: $1.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') + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') Profile clone() => Profile()..mergeFromMessage(this); - @$core.Deprecated( - 'Using this can add significant overhead to your binary. ' - 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' - 'Will be removed in next major version') - Profile copyWith(void Function(Profile) updates) => super.copyWith((message) => updates(message as Profile)) as Profile; + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + Profile copyWith(void Function(Profile) updates) => + super.copyWith((message) => updates(message as Profile)) as Profile; $pb.BuilderInfo get info_ => _i; @@ -397,13 +508,17 @@ class Profile extends $pb.GeneratedMessage { Profile createEmptyInstance() => create(); static $pb.PbList createRepeated() => $pb.PbList(); @$core.pragma('dart2js:noInline') - static Profile getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static Profile getDefault() => + _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); static Profile? _defaultInstance; @$pb.TagNumber(1) $core.String get name => $_getSZ(0); @$pb.TagNumber(1) - set name($core.String v) { $_setString(0, v); } + set name($core.String v) { + $_setString(0, v); + } + @$pb.TagNumber(1) $core.bool hasName() => $_has(0); @$pb.TagNumber(1) @@ -412,7 +527,10 @@ class Profile extends $pb.GeneratedMessage { @$pb.TagNumber(2) $core.String get pronouns => $_getSZ(1); @$pb.TagNumber(2) - set pronouns($core.String v) { $_setString(1, v); } + set pronouns($core.String v) { + $_setString(1, v); + } + @$pb.TagNumber(2) $core.bool hasPronouns() => $_has(1); @$pb.TagNumber(2) @@ -421,7 +539,10 @@ class Profile extends $pb.GeneratedMessage { @$pb.TagNumber(3) $core.String get status => $_getSZ(2); @$pb.TagNumber(3) - set status($core.String v) { $_setString(2, v); } + set status($core.String v) { + $_setString(2, v); + } + @$pb.TagNumber(3) $core.bool hasStatus() => $_has(2); @$pb.TagNumber(3) @@ -430,7 +551,10 @@ class Profile extends $pb.GeneratedMessage { @$pb.TagNumber(4) Availability get availability => $_getN(3); @$pb.TagNumber(4) - set availability(Availability v) { setField(4, v); } + set availability(Availability v) { + setField(4, v); + } + @$pb.TagNumber(4) $core.bool hasAvailability() => $_has(3); @$pb.TagNumber(4) @@ -439,7 +563,10 @@ class Profile extends $pb.GeneratedMessage { @$pb.TagNumber(5) $1.TypedKey get avatar => $_getN(4); @$pb.TagNumber(5) - set avatar($1.TypedKey v) { setField(5, v); } + set avatar($1.TypedKey v) { + setField(5, v); + } + @$pb.TagNumber(5) $core.bool hasAvatar() => $_has(4); @$pb.TagNumber(5) @@ -451,25 +578,34 @@ class Profile extends $pb.GeneratedMessage { 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); + 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', package: const $pb.PackageName(_omitMessageNames ? '' : 'veilidchat'), createEmptyInstance: create) - ..e(1, _omitFieldNames ? '' : 'type', $pb.PbFieldType.OE, defaultOrMaker: ChatType.CHAT_TYPE_UNSPECIFIED, valueOf: ChatType.valueOf, enumValues: ChatType.values) - ..aOM<$1.TypedKey>(2, _omitFieldNames ? '' : 'remoteConversationKey', subBuilder: $1.TypedKey.create) - ..hasRequiredFields = false - ; + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames ? '' : 'Chat', + package: const $pb.PackageName(_omitMessageNames ? '' : 'veilidchat'), + createEmptyInstance: create) + ..e(1, _omitFieldNames ? '' : 'type', $pb.PbFieldType.OE, + defaultOrMaker: ChatType.CHAT_TYPE_UNSPECIFIED, + valueOf: ChatType.valueOf, + enumValues: ChatType.values) + ..aOM<$1.TypedKey>(2, _omitFieldNames ? '' : 'remoteConversationKey', + subBuilder: $1.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') + @$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; + @$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; @@ -478,13 +614,17 @@ class Chat extends $pb.GeneratedMessage { Chat createEmptyInstance() => create(); static $pb.PbList createRepeated() => $pb.PbList(); @$core.pragma('dart2js:noInline') - static Chat getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + 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); } + set type(ChatType v) { + setField(1, v); + } + @$pb.TagNumber(1) $core.bool hasType() => $_has(0); @$pb.TagNumber(1) @@ -493,7 +633,10 @@ class Chat extends $pb.GeneratedMessage { @$pb.TagNumber(2) $1.TypedKey get remoteConversationKey => $_getN(1); @$pb.TagNumber(2) - set remoteConversationKey($1.TypedKey v) { setField(2, v); } + set remoteConversationKey($1.TypedKey v) { + setField(2, v); + } + @$pb.TagNumber(2) $core.bool hasRemoteConversationKey() => $_has(1); @$pb.TagNumber(2) @@ -505,29 +648,40 @@ class Chat extends $pb.GeneratedMessage { class Account extends $pb.GeneratedMessage { factory Account() => create(); Account._() : super(); - factory Account.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); - factory Account.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + factory Account.fromBuffer($core.List<$core.int> i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(i, r); + factory Account.fromJson($core.String i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(i, r); - static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'Account', package: const $pb.PackageName(_omitMessageNames ? '' : 'veilidchat'), createEmptyInstance: create) - ..aOM(1, _omitFieldNames ? '' : 'profile', subBuilder: Profile.create) + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames ? '' : 'Account', + package: const $pb.PackageName(_omitMessageNames ? '' : 'veilidchat'), + createEmptyInstance: create) + ..aOM(1, _omitFieldNames ? '' : 'profile', + subBuilder: Profile.create) ..aOB(2, _omitFieldNames ? '' : 'invisible') - ..a<$core.int>(3, _omitFieldNames ? '' : 'autoAwayTimeoutSec', $pb.PbFieldType.OU3) - ..aOM<$0.OwnedDHTRecordPointer>(4, _omitFieldNames ? '' : 'contactList', subBuilder: $0.OwnedDHTRecordPointer.create) - ..aOM<$0.OwnedDHTRecordPointer>(5, _omitFieldNames ? '' : 'contactInvitationRecords', subBuilder: $0.OwnedDHTRecordPointer.create) - ..aOM<$0.OwnedDHTRecordPointer>(6, _omitFieldNames ? '' : 'chatList', subBuilder: $0.OwnedDHTRecordPointer.create) - ..hasRequiredFields = false - ; + ..a<$core.int>( + 3, _omitFieldNames ? '' : 'autoAwayTimeoutSec', $pb.PbFieldType.OU3) + ..aOM<$0.OwnedDHTRecordPointer>(4, _omitFieldNames ? '' : 'contactList', + subBuilder: $0.OwnedDHTRecordPointer.create) + ..aOM<$0.OwnedDHTRecordPointer>( + 5, _omitFieldNames ? '' : 'contactInvitationRecords', + subBuilder: $0.OwnedDHTRecordPointer.create) + ..aOM<$0.OwnedDHTRecordPointer>(6, _omitFieldNames ? '' : 'chatList', + subBuilder: $0.OwnedDHTRecordPointer.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') + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') Account clone() => Account()..mergeFromMessage(this); - @$core.Deprecated( - 'Using this can add significant overhead to your binary. ' - 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' - 'Will be removed in next major version') - Account copyWith(void Function(Account) updates) => super.copyWith((message) => updates(message as Account)) as Account; + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + Account copyWith(void Function(Account) updates) => + super.copyWith((message) => updates(message as Account)) as Account; $pb.BuilderInfo get info_ => _i; @@ -536,13 +690,17 @@ class Account extends $pb.GeneratedMessage { Account createEmptyInstance() => create(); static $pb.PbList createRepeated() => $pb.PbList(); @$core.pragma('dart2js:noInline') - static Account getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static Account getDefault() => + _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); static Account? _defaultInstance; @$pb.TagNumber(1) Profile get profile => $_getN(0); @$pb.TagNumber(1) - set profile(Profile v) { setField(1, v); } + set profile(Profile v) { + setField(1, v); + } + @$pb.TagNumber(1) $core.bool hasProfile() => $_has(0); @$pb.TagNumber(1) @@ -553,7 +711,10 @@ class Account extends $pb.GeneratedMessage { @$pb.TagNumber(2) $core.bool get invisible => $_getBF(1); @$pb.TagNumber(2) - set invisible($core.bool v) { $_setBool(1, v); } + set invisible($core.bool v) { + $_setBool(1, v); + } + @$pb.TagNumber(2) $core.bool hasInvisible() => $_has(1); @$pb.TagNumber(2) @@ -562,7 +723,10 @@ class Account extends $pb.GeneratedMessage { @$pb.TagNumber(3) $core.int get autoAwayTimeoutSec => $_getIZ(2); @$pb.TagNumber(3) - set autoAwayTimeoutSec($core.int v) { $_setUnsignedInt32(2, v); } + set autoAwayTimeoutSec($core.int v) { + $_setUnsignedInt32(2, v); + } + @$pb.TagNumber(3) $core.bool hasAutoAwayTimeoutSec() => $_has(2); @$pb.TagNumber(3) @@ -571,7 +735,10 @@ class Account extends $pb.GeneratedMessage { @$pb.TagNumber(4) $0.OwnedDHTRecordPointer get contactList => $_getN(3); @$pb.TagNumber(4) - set contactList($0.OwnedDHTRecordPointer v) { setField(4, v); } + set contactList($0.OwnedDHTRecordPointer v) { + setField(4, v); + } + @$pb.TagNumber(4) $core.bool hasContactList() => $_has(3); @$pb.TagNumber(4) @@ -582,7 +749,10 @@ class Account extends $pb.GeneratedMessage { @$pb.TagNumber(5) $0.OwnedDHTRecordPointer get contactInvitationRecords => $_getN(4); @$pb.TagNumber(5) - set contactInvitationRecords($0.OwnedDHTRecordPointer v) { setField(5, v); } + set contactInvitationRecords($0.OwnedDHTRecordPointer v) { + setField(5, v); + } + @$pb.TagNumber(5) $core.bool hasContactInvitationRecords() => $_has(4); @$pb.TagNumber(5) @@ -593,7 +763,10 @@ class Account extends $pb.GeneratedMessage { @$pb.TagNumber(6) $0.OwnedDHTRecordPointer get chatList => $_getN(5); @$pb.TagNumber(6) - set chatList($0.OwnedDHTRecordPointer v) { setField(6, v); } + set chatList($0.OwnedDHTRecordPointer v) { + setField(6, v); + } + @$pb.TagNumber(6) $core.bool hasChatList() => $_has(5); @$pb.TagNumber(6) @@ -605,40 +778,53 @@ class Account extends $pb.GeneratedMessage { class ContactInvitation extends $pb.GeneratedMessage { factory ContactInvitation() => create(); ContactInvitation._() : super(); - factory ContactInvitation.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); - factory ContactInvitation.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + factory ContactInvitation.fromBuffer($core.List<$core.int> i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(i, r); + factory ContactInvitation.fromJson($core.String i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(i, r); - static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'ContactInvitation', package: const $pb.PackageName(_omitMessageNames ? '' : 'veilidchat'), createEmptyInstance: create) - ..aOM<$1.TypedKey>(1, _omitFieldNames ? '' : 'contactRequestInboxKey', subBuilder: $1.TypedKey.create) - ..a<$core.List<$core.int>>(2, _omitFieldNames ? '' : 'writerSecret', $pb.PbFieldType.OY) - ..hasRequiredFields = false - ; + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames ? '' : 'ContactInvitation', + package: const $pb.PackageName(_omitMessageNames ? '' : 'veilidchat'), + createEmptyInstance: create) + ..aOM<$1.TypedKey>(1, _omitFieldNames ? '' : 'contactRequestInboxKey', + subBuilder: $1.TypedKey.create) + ..a<$core.List<$core.int>>( + 2, _omitFieldNames ? '' : 'writerSecret', $pb.PbFieldType.OY) + ..hasRequiredFields = false; - @$core.Deprecated( - 'Using this can add significant overhead to your binary. ' - 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' - 'Will be removed in next major version') + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') ContactInvitation clone() => ContactInvitation()..mergeFromMessage(this); - @$core.Deprecated( - 'Using this can add significant overhead to your binary. ' - 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' - 'Will be removed in next major version') - ContactInvitation copyWith(void Function(ContactInvitation) updates) => super.copyWith((message) => updates(message as ContactInvitation)) as ContactInvitation; + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + ContactInvitation copyWith(void Function(ContactInvitation) updates) => + super.copyWith((message) => updates(message as ContactInvitation)) + as ContactInvitation; $pb.BuilderInfo get info_ => _i; @$core.pragma('dart2js:noInline') static ContactInvitation create() => ContactInvitation._(); ContactInvitation createEmptyInstance() => create(); - static $pb.PbList createRepeated() => $pb.PbList(); + static $pb.PbList createRepeated() => + $pb.PbList(); @$core.pragma('dart2js:noInline') - static ContactInvitation getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static ContactInvitation getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor(create); static ContactInvitation? _defaultInstance; @$pb.TagNumber(1) $1.TypedKey get contactRequestInboxKey => $_getN(0); @$pb.TagNumber(1) - set contactRequestInboxKey($1.TypedKey v) { setField(1, v); } + set contactRequestInboxKey($1.TypedKey v) { + setField(1, v); + } + @$pb.TagNumber(1) $core.bool hasContactRequestInboxKey() => $_has(0); @$pb.TagNumber(1) @@ -649,7 +835,10 @@ class ContactInvitation extends $pb.GeneratedMessage { @$pb.TagNumber(2) $core.List<$core.int> get writerSecret => $_getN(1); @$pb.TagNumber(2) - set writerSecret($core.List<$core.int> v) { $_setBytes(1, v); } + set writerSecret($core.List<$core.int> v) { + $_setBytes(1, v); + } + @$pb.TagNumber(2) $core.bool hasWriterSecret() => $_has(1); @$pb.TagNumber(2) @@ -659,40 +848,55 @@ class ContactInvitation extends $pb.GeneratedMessage { class SignedContactInvitation extends $pb.GeneratedMessage { factory SignedContactInvitation() => create(); SignedContactInvitation._() : super(); - factory SignedContactInvitation.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); - factory SignedContactInvitation.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + factory SignedContactInvitation.fromBuffer($core.List<$core.int> i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(i, r); + factory SignedContactInvitation.fromJson($core.String i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(i, r); - static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'SignedContactInvitation', package: const $pb.PackageName(_omitMessageNames ? '' : 'veilidchat'), createEmptyInstance: create) - ..a<$core.List<$core.int>>(1, _omitFieldNames ? '' : 'contactInvitation', $pb.PbFieldType.OY) - ..aOM<$1.Signature>(2, _omitFieldNames ? '' : 'identitySignature', subBuilder: $1.Signature.create) - ..hasRequiredFields = false - ; + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames ? '' : 'SignedContactInvitation', + package: const $pb.PackageName(_omitMessageNames ? '' : 'veilidchat'), + createEmptyInstance: create) + ..a<$core.List<$core.int>>( + 1, _omitFieldNames ? '' : 'contactInvitation', $pb.PbFieldType.OY) + ..aOM<$1.Signature>(2, _omitFieldNames ? '' : 'identitySignature', + subBuilder: $1.Signature.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') - SignedContactInvitation clone() => SignedContactInvitation()..mergeFromMessage(this); - @$core.Deprecated( - 'Using this can add significant overhead to your binary. ' - 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' - 'Will be removed in next major version') - SignedContactInvitation copyWith(void Function(SignedContactInvitation) updates) => super.copyWith((message) => updates(message as SignedContactInvitation)) as SignedContactInvitation; + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + SignedContactInvitation clone() => + SignedContactInvitation()..mergeFromMessage(this); + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + SignedContactInvitation copyWith( + void Function(SignedContactInvitation) updates) => + super.copyWith((message) => updates(message as SignedContactInvitation)) + as SignedContactInvitation; $pb.BuilderInfo get info_ => _i; @$core.pragma('dart2js:noInline') static SignedContactInvitation create() => SignedContactInvitation._(); SignedContactInvitation createEmptyInstance() => create(); - static $pb.PbList createRepeated() => $pb.PbList(); + static $pb.PbList createRepeated() => + $pb.PbList(); @$core.pragma('dart2js:noInline') - static SignedContactInvitation getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static SignedContactInvitation getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor(create); static SignedContactInvitation? _defaultInstance; @$pb.TagNumber(1) $core.List<$core.int> get contactInvitation => $_getN(0); @$pb.TagNumber(1) - set contactInvitation($core.List<$core.int> v) { $_setBytes(0, v); } + set contactInvitation($core.List<$core.int> v) { + $_setBytes(0, v); + } + @$pb.TagNumber(1) $core.bool hasContactInvitation() => $_has(0); @$pb.TagNumber(1) @@ -701,7 +905,10 @@ class SignedContactInvitation extends $pb.GeneratedMessage { @$pb.TagNumber(2) $1.Signature get identitySignature => $_getN(1); @$pb.TagNumber(2) - set identitySignature($1.Signature v) { setField(2, v); } + set identitySignature($1.Signature v) { + setField(2, v); + } + @$pb.TagNumber(2) $core.bool hasIdentitySignature() => $_has(1); @$pb.TagNumber(2) @@ -713,40 +920,56 @@ class SignedContactInvitation extends $pb.GeneratedMessage { class ContactRequest extends $pb.GeneratedMessage { factory ContactRequest() => create(); ContactRequest._() : super(); - factory ContactRequest.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); - factory ContactRequest.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + factory ContactRequest.fromBuffer($core.List<$core.int> i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(i, r); + 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', package: const $pb.PackageName(_omitMessageNames ? '' : 'veilidchat'), createEmptyInstance: create) - ..e(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 - ; + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames ? '' : 'ContactRequest', + package: const $pb.PackageName(_omitMessageNames ? '' : 'veilidchat'), + createEmptyInstance: create) + ..e( + 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; - @$core.Deprecated( - 'Using this can add significant overhead to your binary. ' - 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' - 'Will be removed in next major version') + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') ContactRequest clone() => ContactRequest()..mergeFromMessage(this); - @$core.Deprecated( - 'Using this can add significant overhead to your binary. ' - 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' - 'Will be removed in next major version') - ContactRequest copyWith(void Function(ContactRequest) updates) => super.copyWith((message) => updates(message as ContactRequest)) as ContactRequest; + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + ContactRequest copyWith(void Function(ContactRequest) updates) => + super.copyWith((message) => updates(message as ContactRequest)) + as ContactRequest; $pb.BuilderInfo get info_ => _i; @$core.pragma('dart2js:noInline') static ContactRequest create() => ContactRequest._(); ContactRequest createEmptyInstance() => create(); - static $pb.PbList createRepeated() => $pb.PbList(); + static $pb.PbList createRepeated() => + $pb.PbList(); @$core.pragma('dart2js:noInline') - static ContactRequest getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static ContactRequest getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor(create); static ContactRequest? _defaultInstance; @$pb.TagNumber(1) EncryptionKeyType get encryptionKeyType => $_getN(0); @$pb.TagNumber(1) - set encryptionKeyType(EncryptionKeyType v) { setField(1, v); } + set encryptionKeyType(EncryptionKeyType v) { + setField(1, v); + } + @$pb.TagNumber(1) $core.bool hasEncryptionKeyType() => $_has(0); @$pb.TagNumber(1) @@ -755,7 +978,10 @@ class ContactRequest extends $pb.GeneratedMessage { @$pb.TagNumber(2) $core.List<$core.int> get private => $_getN(1); @$pb.TagNumber(2) - set private($core.List<$core.int> v) { $_setBytes(1, v); } + set private($core.List<$core.int> v) { + $_setBytes(1, v); + } + @$pb.TagNumber(2) $core.bool hasPrivate() => $_has(1); @$pb.TagNumber(2) @@ -765,43 +991,62 @@ class ContactRequest extends $pb.GeneratedMessage { class ContactRequestPrivate extends $pb.GeneratedMessage { factory ContactRequestPrivate() => create(); ContactRequestPrivate._() : super(); - factory ContactRequestPrivate.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); - factory ContactRequestPrivate.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + factory ContactRequestPrivate.fromBuffer($core.List<$core.int> i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(i, r); + factory ContactRequestPrivate.fromJson($core.String i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(i, r); - static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'ContactRequestPrivate', package: const $pb.PackageName(_omitMessageNames ? '' : 'veilidchat'), createEmptyInstance: create) - ..aOM<$1.CryptoKey>(1, _omitFieldNames ? '' : 'writerKey', subBuilder: $1.CryptoKey.create) - ..aOM(2, _omitFieldNames ? '' : 'profile', subBuilder: Profile.create) - ..aOM<$1.TypedKey>(3, _omitFieldNames ? '' : 'identityMasterRecordKey', subBuilder: $1.TypedKey.create) - ..aOM<$1.TypedKey>(4, _omitFieldNames ? '' : 'chatRecordKey', subBuilder: $1.TypedKey.create) - ..a<$fixnum.Int64>(5, _omitFieldNames ? '' : 'expiration', $pb.PbFieldType.OU6, defaultOrMaker: $fixnum.Int64.ZERO) - ..hasRequiredFields = false - ; + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames ? '' : 'ContactRequestPrivate', + package: const $pb.PackageName(_omitMessageNames ? '' : 'veilidchat'), + createEmptyInstance: create) + ..aOM<$1.CryptoKey>(1, _omitFieldNames ? '' : 'writerKey', + subBuilder: $1.CryptoKey.create) + ..aOM(2, _omitFieldNames ? '' : 'profile', + subBuilder: Profile.create) + ..aOM<$1.TypedKey>(3, _omitFieldNames ? '' : 'identityMasterRecordKey', + subBuilder: $1.TypedKey.create) + ..aOM<$1.TypedKey>(4, _omitFieldNames ? '' : 'chatRecordKey', + subBuilder: $1.TypedKey.create) + ..a<$fixnum.Int64>( + 5, _omitFieldNames ? '' : 'expiration', $pb.PbFieldType.OU6, + defaultOrMaker: $fixnum.Int64.ZERO) + ..hasRequiredFields = false; - @$core.Deprecated( - 'Using this can add significant overhead to your binary. ' - 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' - 'Will be removed in next major version') - ContactRequestPrivate clone() => ContactRequestPrivate()..mergeFromMessage(this); - @$core.Deprecated( - 'Using this can add significant overhead to your binary. ' - 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' - 'Will be removed in next major version') - ContactRequestPrivate copyWith(void Function(ContactRequestPrivate) updates) => super.copyWith((message) => updates(message as ContactRequestPrivate)) as ContactRequestPrivate; + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + ContactRequestPrivate clone() => + ContactRequestPrivate()..mergeFromMessage(this); + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + ContactRequestPrivate copyWith( + void Function(ContactRequestPrivate) updates) => + super.copyWith((message) => updates(message as ContactRequestPrivate)) + as ContactRequestPrivate; $pb.BuilderInfo get info_ => _i; @$core.pragma('dart2js:noInline') static ContactRequestPrivate create() => ContactRequestPrivate._(); ContactRequestPrivate createEmptyInstance() => create(); - static $pb.PbList createRepeated() => $pb.PbList(); + static $pb.PbList createRepeated() => + $pb.PbList(); @$core.pragma('dart2js:noInline') - static ContactRequestPrivate getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static ContactRequestPrivate getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor(create); static ContactRequestPrivate? _defaultInstance; @$pb.TagNumber(1) $1.CryptoKey get writerKey => $_getN(0); @$pb.TagNumber(1) - set writerKey($1.CryptoKey v) { setField(1, v); } + set writerKey($1.CryptoKey v) { + setField(1, v); + } + @$pb.TagNumber(1) $core.bool hasWriterKey() => $_has(0); @$pb.TagNumber(1) @@ -812,7 +1057,10 @@ class ContactRequestPrivate extends $pb.GeneratedMessage { @$pb.TagNumber(2) Profile get profile => $_getN(1); @$pb.TagNumber(2) - set profile(Profile v) { setField(2, v); } + set profile(Profile v) { + setField(2, v); + } + @$pb.TagNumber(2) $core.bool hasProfile() => $_has(1); @$pb.TagNumber(2) @@ -823,7 +1071,10 @@ class ContactRequestPrivate extends $pb.GeneratedMessage { @$pb.TagNumber(3) $1.TypedKey get identityMasterRecordKey => $_getN(2); @$pb.TagNumber(3) - set identityMasterRecordKey($1.TypedKey v) { setField(3, v); } + set identityMasterRecordKey($1.TypedKey v) { + setField(3, v); + } + @$pb.TagNumber(3) $core.bool hasIdentityMasterRecordKey() => $_has(2); @$pb.TagNumber(3) @@ -834,7 +1085,10 @@ class ContactRequestPrivate extends $pb.GeneratedMessage { @$pb.TagNumber(4) $1.TypedKey get chatRecordKey => $_getN(3); @$pb.TagNumber(4) - set chatRecordKey($1.TypedKey v) { setField(4, v); } + set chatRecordKey($1.TypedKey v) { + setField(4, v); + } + @$pb.TagNumber(4) $core.bool hasChatRecordKey() => $_has(3); @$pb.TagNumber(4) @@ -845,7 +1099,10 @@ class ContactRequestPrivate extends $pb.GeneratedMessage { @$pb.TagNumber(5) $fixnum.Int64 get expiration => $_getI64(4); @$pb.TagNumber(5) - set expiration($fixnum.Int64 v) { $_setInt64(4, v); } + set expiration($fixnum.Int64 v) { + $_setInt64(4, v); + } + @$pb.TagNumber(5) $core.bool hasExpiration() => $_has(4); @$pb.TagNumber(5) @@ -855,41 +1112,54 @@ class ContactRequestPrivate extends $pb.GeneratedMessage { class ContactResponse extends $pb.GeneratedMessage { factory ContactResponse() => create(); ContactResponse._() : super(); - factory ContactResponse.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); - factory ContactResponse.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + factory ContactResponse.fromBuffer($core.List<$core.int> i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(i, r); + factory ContactResponse.fromJson($core.String i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(i, r); - static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'ContactResponse', package: const $pb.PackageName(_omitMessageNames ? '' : 'veilidchat'), createEmptyInstance: create) + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames ? '' : 'ContactResponse', + package: const $pb.PackageName(_omitMessageNames ? '' : 'veilidchat'), + createEmptyInstance: create) ..aOB(1, _omitFieldNames ? '' : 'accept') - ..aOM<$1.TypedKey>(2, _omitFieldNames ? '' : 'identityMasterRecordKey', subBuilder: $1.TypedKey.create) - ..aOM<$1.TypedKey>(3, _omitFieldNames ? '' : 'remoteConversationRecordKey', subBuilder: $1.TypedKey.create) - ..hasRequiredFields = false - ; + ..aOM<$1.TypedKey>(2, _omitFieldNames ? '' : 'identityMasterRecordKey', + subBuilder: $1.TypedKey.create) + ..aOM<$1.TypedKey>(3, _omitFieldNames ? '' : 'remoteConversationRecordKey', + subBuilder: $1.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') + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') ContactResponse clone() => ContactResponse()..mergeFromMessage(this); - @$core.Deprecated( - 'Using this can add significant overhead to your binary. ' - 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' - 'Will be removed in next major version') - ContactResponse copyWith(void Function(ContactResponse) updates) => super.copyWith((message) => updates(message as ContactResponse)) as ContactResponse; + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + ContactResponse copyWith(void Function(ContactResponse) updates) => + super.copyWith((message) => updates(message as ContactResponse)) + as ContactResponse; $pb.BuilderInfo get info_ => _i; @$core.pragma('dart2js:noInline') static ContactResponse create() => ContactResponse._(); ContactResponse createEmptyInstance() => create(); - static $pb.PbList createRepeated() => $pb.PbList(); + static $pb.PbList createRepeated() => + $pb.PbList(); @$core.pragma('dart2js:noInline') - static ContactResponse getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static ContactResponse getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor(create); static ContactResponse? _defaultInstance; @$pb.TagNumber(1) $core.bool get accept => $_getBF(0); @$pb.TagNumber(1) - set accept($core.bool v) { $_setBool(0, v); } + set accept($core.bool v) { + $_setBool(0, v); + } + @$pb.TagNumber(1) $core.bool hasAccept() => $_has(0); @$pb.TagNumber(1) @@ -898,7 +1168,10 @@ class ContactResponse extends $pb.GeneratedMessage { @$pb.TagNumber(2) $1.TypedKey get identityMasterRecordKey => $_getN(1); @$pb.TagNumber(2) - set identityMasterRecordKey($1.TypedKey v) { setField(2, v); } + set identityMasterRecordKey($1.TypedKey v) { + setField(2, v); + } + @$pb.TagNumber(2) $core.bool hasIdentityMasterRecordKey() => $_has(1); @$pb.TagNumber(2) @@ -909,7 +1182,10 @@ class ContactResponse extends $pb.GeneratedMessage { @$pb.TagNumber(3) $1.TypedKey get remoteConversationRecordKey => $_getN(2); @$pb.TagNumber(3) - set remoteConversationRecordKey($1.TypedKey v) { setField(3, v); } + set remoteConversationRecordKey($1.TypedKey v) { + setField(3, v); + } + @$pb.TagNumber(3) $core.bool hasRemoteConversationRecordKey() => $_has(2); @$pb.TagNumber(3) @@ -921,40 +1197,55 @@ class ContactResponse extends $pb.GeneratedMessage { class SignedContactResponse extends $pb.GeneratedMessage { factory SignedContactResponse() => create(); SignedContactResponse._() : super(); - factory SignedContactResponse.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); - factory SignedContactResponse.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + factory SignedContactResponse.fromBuffer($core.List<$core.int> i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(i, r); + factory SignedContactResponse.fromJson($core.String i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(i, r); - static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'SignedContactResponse', package: const $pb.PackageName(_omitMessageNames ? '' : 'veilidchat'), createEmptyInstance: create) - ..a<$core.List<$core.int>>(1, _omitFieldNames ? '' : 'contactResponse', $pb.PbFieldType.OY) - ..aOM<$1.Signature>(2, _omitFieldNames ? '' : 'identitySignature', subBuilder: $1.Signature.create) - ..hasRequiredFields = false - ; + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames ? '' : 'SignedContactResponse', + package: const $pb.PackageName(_omitMessageNames ? '' : 'veilidchat'), + createEmptyInstance: create) + ..a<$core.List<$core.int>>( + 1, _omitFieldNames ? '' : 'contactResponse', $pb.PbFieldType.OY) + ..aOM<$1.Signature>(2, _omitFieldNames ? '' : 'identitySignature', + subBuilder: $1.Signature.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') - SignedContactResponse clone() => SignedContactResponse()..mergeFromMessage(this); - @$core.Deprecated( - 'Using this can add significant overhead to your binary. ' - 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' - 'Will be removed in next major version') - SignedContactResponse copyWith(void Function(SignedContactResponse) updates) => super.copyWith((message) => updates(message as SignedContactResponse)) as SignedContactResponse; + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + SignedContactResponse clone() => + SignedContactResponse()..mergeFromMessage(this); + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + SignedContactResponse copyWith( + void Function(SignedContactResponse) updates) => + super.copyWith((message) => updates(message as SignedContactResponse)) + as SignedContactResponse; $pb.BuilderInfo get info_ => _i; @$core.pragma('dart2js:noInline') static SignedContactResponse create() => SignedContactResponse._(); SignedContactResponse createEmptyInstance() => create(); - static $pb.PbList createRepeated() => $pb.PbList(); + static $pb.PbList createRepeated() => + $pb.PbList(); @$core.pragma('dart2js:noInline') - static SignedContactResponse getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static SignedContactResponse getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor(create); static SignedContactResponse? _defaultInstance; @$pb.TagNumber(1) $core.List<$core.int> get contactResponse => $_getN(0); @$pb.TagNumber(1) - set contactResponse($core.List<$core.int> v) { $_setBytes(0, v); } + set contactResponse($core.List<$core.int> v) { + $_setBytes(0, v); + } + @$pb.TagNumber(1) $core.bool hasContactResponse() => $_has(0); @$pb.TagNumber(1) @@ -963,7 +1254,10 @@ class SignedContactResponse extends $pb.GeneratedMessage { @$pb.TagNumber(2) $1.Signature get identitySignature => $_getN(1); @$pb.TagNumber(2) - set identitySignature($1.Signature v) { setField(2, v); } + set identitySignature($1.Signature v) { + setField(2, v); + } + @$pb.TagNumber(2) $core.bool hasIdentitySignature() => $_has(1); @$pb.TagNumber(2) @@ -975,45 +1269,66 @@ class SignedContactResponse extends $pb.GeneratedMessage { 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); + 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 ? '' : 'ContactInvitationRecord', package: const $pb.PackageName(_omitMessageNames ? '' : 'veilidchat'), createEmptyInstance: create) - ..aOM<$0.OwnedDHTRecordPointer>(1, _omitFieldNames ? '' : 'contactRequestInbox', subBuilder: $0.OwnedDHTRecordPointer.create) - ..aOM<$1.CryptoKey>(2, _omitFieldNames ? '' : 'writerKey', subBuilder: $1.CryptoKey.create) - ..aOM<$1.CryptoKey>(3, _omitFieldNames ? '' : 'writerSecret', subBuilder: $1.CryptoKey.create) - ..aOM<$1.TypedKey>(4, _omitFieldNames ? '' : 'localConversationRecordKey', subBuilder: $1.TypedKey.create) - ..a<$fixnum.Int64>(5, _omitFieldNames ? '' : 'expiration', $pb.PbFieldType.OU6, defaultOrMaker: $fixnum.Int64.ZERO) - ..a<$core.List<$core.int>>(6, _omitFieldNames ? '' : 'invitation', $pb.PbFieldType.OY) + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames ? '' : 'ContactInvitationRecord', + package: const $pb.PackageName(_omitMessageNames ? '' : 'veilidchat'), + createEmptyInstance: create) + ..aOM<$0.OwnedDHTRecordPointer>( + 1, _omitFieldNames ? '' : 'contactRequestInbox', + subBuilder: $0.OwnedDHTRecordPointer.create) + ..aOM<$1.CryptoKey>(2, _omitFieldNames ? '' : 'writerKey', + subBuilder: $1.CryptoKey.create) + ..aOM<$1.CryptoKey>(3, _omitFieldNames ? '' : 'writerSecret', + subBuilder: $1.CryptoKey.create) + ..aOM<$1.TypedKey>(4, _omitFieldNames ? '' : 'localConversationRecordKey', + subBuilder: $1.TypedKey.create) + ..a<$fixnum.Int64>( + 5, _omitFieldNames ? '' : 'expiration', $pb.PbFieldType.OU6, + defaultOrMaker: $fixnum.Int64.ZERO) + ..a<$core.List<$core.int>>( + 6, _omitFieldNames ? '' : 'invitation', $pb.PbFieldType.OY) ..aOS(7, _omitFieldNames ? '' : 'message') - ..hasRequiredFields = false - ; + ..hasRequiredFields = false; - @$core.Deprecated( - 'Using this can add significant overhead to your binary. ' - 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' - 'Will be removed in next major version') - 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') - ContactInvitationRecord copyWith(void Function(ContactInvitationRecord) updates) => super.copyWith((message) => updates(message as ContactInvitationRecord)) as ContactInvitationRecord; + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + 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') + 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 ContactInvitationRecord create() => ContactInvitationRecord._(); ContactInvitationRecord createEmptyInstance() => create(); - static $pb.PbList createRepeated() => $pb.PbList(); + static $pb.PbList createRepeated() => + $pb.PbList(); @$core.pragma('dart2js:noInline') - static ContactInvitationRecord getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static ContactInvitationRecord getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor(create); static ContactInvitationRecord? _defaultInstance; @$pb.TagNumber(1) $0.OwnedDHTRecordPointer get contactRequestInbox => $_getN(0); @$pb.TagNumber(1) - set contactRequestInbox($0.OwnedDHTRecordPointer v) { setField(1, v); } + set contactRequestInbox($0.OwnedDHTRecordPointer v) { + setField(1, v); + } + @$pb.TagNumber(1) $core.bool hasContactRequestInbox() => $_has(0); @$pb.TagNumber(1) @@ -1024,7 +1339,10 @@ class ContactInvitationRecord extends $pb.GeneratedMessage { @$pb.TagNumber(2) $1.CryptoKey get writerKey => $_getN(1); @$pb.TagNumber(2) - set writerKey($1.CryptoKey v) { setField(2, v); } + set writerKey($1.CryptoKey v) { + setField(2, v); + } + @$pb.TagNumber(2) $core.bool hasWriterKey() => $_has(1); @$pb.TagNumber(2) @@ -1035,7 +1353,10 @@ class ContactInvitationRecord extends $pb.GeneratedMessage { @$pb.TagNumber(3) $1.CryptoKey get writerSecret => $_getN(2); @$pb.TagNumber(3) - set writerSecret($1.CryptoKey v) { setField(3, v); } + set writerSecret($1.CryptoKey v) { + setField(3, v); + } + @$pb.TagNumber(3) $core.bool hasWriterSecret() => $_has(2); @$pb.TagNumber(3) @@ -1046,7 +1367,10 @@ class ContactInvitationRecord extends $pb.GeneratedMessage { @$pb.TagNumber(4) $1.TypedKey get localConversationRecordKey => $_getN(3); @$pb.TagNumber(4) - set localConversationRecordKey($1.TypedKey v) { setField(4, v); } + set localConversationRecordKey($1.TypedKey v) { + setField(4, v); + } + @$pb.TagNumber(4) $core.bool hasLocalConversationRecordKey() => $_has(3); @$pb.TagNumber(4) @@ -1057,7 +1381,10 @@ class ContactInvitationRecord extends $pb.GeneratedMessage { @$pb.TagNumber(5) $fixnum.Int64 get expiration => $_getI64(4); @$pb.TagNumber(5) - set expiration($fixnum.Int64 v) { $_setInt64(4, v); } + set expiration($fixnum.Int64 v) { + $_setInt64(4, v); + } + @$pb.TagNumber(5) $core.bool hasExpiration() => $_has(4); @$pb.TagNumber(5) @@ -1066,7 +1393,10 @@ class ContactInvitationRecord extends $pb.GeneratedMessage { @$pb.TagNumber(6) $core.List<$core.int> get invitation => $_getN(5); @$pb.TagNumber(6) - set invitation($core.List<$core.int> v) { $_setBytes(5, v); } + set invitation($core.List<$core.int> v) { + $_setBytes(5, v); + } + @$pb.TagNumber(6) $core.bool hasInvitation() => $_has(5); @$pb.TagNumber(6) @@ -1075,13 +1405,16 @@ class ContactInvitationRecord extends $pb.GeneratedMessage { @$pb.TagNumber(7) $core.String get message => $_getSZ(6); @$pb.TagNumber(7) - set message($core.String v) { $_setString(6, v); } + set message($core.String v) { + $_setString(6, v); + } + @$pb.TagNumber(7) $core.bool hasMessage() => $_has(6); @$pb.TagNumber(7) void clearMessage() => clearField(7); } - const _omitFieldNames = $core.bool.fromEnvironment('protobuf.omit_field_names'); -const _omitMessageNames = $core.bool.fromEnvironment('protobuf.omit_message_names'); +const _omitMessageNames = + $core.bool.fromEnvironment('protobuf.omit_message_names'); diff --git a/lib/router/cubit/router_cubit.dart b/lib/router/cubit/router_cubit.dart index 8aec72a..b0ba3eb 100644 --- a/lib/router/cubit/router_cubit.dart +++ b/lib/router/cubit/router_cubit.dart @@ -1,13 +1,16 @@ import 'dart:async'; import 'package:bloc/bloc.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:go_router/go_router.dart'; +import 'package:stream_transform/stream_transform.dart'; import '../../../account_manager/account_manager.dart'; import '../../init.dart'; import '../../layout/layout.dart'; +import '../../settings/settings.dart'; import '../../tools/tools.dart'; import '../../veilid_processor/views/developer.dart'; @@ -15,6 +18,11 @@ part 'router_cubit.freezed.dart'; part 'router_cubit.g.dart'; part 'router_state.dart'; +final _rootNavKey = GlobalKey(debugLabel: 'rootNavKey'); +final _homeNavKey = GlobalKey(debugLabel: 'homeNavKey'); +final _readyAccountNavKey = + GlobalKey(debugLabel: 'readyAccountNavKey'); + class RouterCubit extends Cubit { RouterCubit(AccountRepository accountRepository) : super(const RouterState( @@ -27,9 +35,9 @@ class RouterCubit extends Cubit { await eventualInitialized.future; emit(state.copyWith(isInitialized: true)); }); + // Subscribe to repository streams - _accountRepositorySubscription = - accountRepository.stream().listen((event) { + _accountRepositorySubscription = accountRepository.stream.listen((event) { switch (event) { case AccountRepositoryChange.localAccounts: emit(state.copyWith( @@ -40,7 +48,6 @@ class RouterCubit extends Cubit { break; } }); - _chatListRepositorySubscription = ... } @override @@ -50,34 +57,50 @@ class RouterCubit extends Cubit { } /// Our application routes - List get routes => [ + List get routes => [ GoRoute( path: '/', builder: (context, state) => const IndexPage(), ), - GoRoute( - path: '/home', - builder: (context, state) => const HomePage(), - routes: [ - GoRoute( - path: 'settings', - builder: (context, state) => const SettingsPage(), - ), - GoRoute( - path: 'chat', - builder: (context, state) => const ChatOnlyPage(), - ), - ], - ), + ShellRoute( + navigatorKey: _homeNavKey, + builder: (context, state, child) => HomeShell(child: child), + routes: [ + GoRoute( + path: '/home/no_active', + builder: (context, state) => const HomeNoActive(), + ), + GoRoute( + path: '/home/account_missing', + builder: (context, state) => const HomeAccountMissing(), + ), + GoRoute( + path: '/home/account_locked', + builder: (context, state) => const HomeAccountLocked(), + ), + ShellRoute( + navigatorKey: _readyAccountNavKey, + builder: (context, state, child) => + HomeAccountReadyShell(child: child), + routes: [ + GoRoute( + path: '/home', + builder: (context, state) => const HomeAccountReadyMain(), + ), + GoRoute( + path: '/home/chat', + builder: (context, state) => const HomeAccountReadyChat(), + ), + ], + ), + ]), GoRoute( path: '/new_account', builder: (context, state) => const NewAccountPage(), - routes: [ - GoRoute( - path: 'settings', - builder: (context, state) => const SettingsPage(), - ), - ], + ), + GoRoute( + path: '/settings', + builder: (context, state) => const SettingsPage(), ), GoRoute( path: '/developer', @@ -87,11 +110,8 @@ class RouterCubit extends Cubit { /// Redirects when our state changes String? redirect(BuildContext context, GoRouterState goRouterState) { - // if (state.isLoading || state.hasError) { - // return null; - // } - // No matter where we are, if there's not + switch (goRouterState.matchedLocation) { case '/': @@ -133,8 +153,7 @@ class RouterCubit extends Cubit { return '/home'; } return null; - case '/home/settings': - case '/new_account/settings': + case '/settings': return null; case '/developer': return null; @@ -143,6 +162,18 @@ class RouterCubit extends Cubit { } } + /// Make a GoRouter instance that uses this cubit + GoRouter router() => GoRouter( + navigatorKey: _rootNavKey, + refreshListenable: StreamListenable(stream.startWith(state).distinct()), + debugLogDiagnostics: kDebugMode, + initialLocation: '/', + routes: routes, + redirect: redirect, + ); + + //////////////// + late final StreamSubscription _accountRepositorySubscription; } diff --git a/lib/router/cubit/router_cubit.freezed.dart b/lib/router/cubit/router_cubit.freezed.dart new file mode 100644 index 0000000..16baed7 --- /dev/null +++ b/lib/router/cubit/router_cubit.freezed.dart @@ -0,0 +1,193 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'router_cubit.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +final _privateConstructorUsedError = UnsupportedError( + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); + +RouterState _$RouterStateFromJson(Map json) { + return _RouterState.fromJson(json); +} + +/// @nodoc +mixin _$RouterState { + bool get isInitialized => throw _privateConstructorUsedError; + bool get hasAnyAccount => throw _privateConstructorUsedError; + bool get hasActiveChat => throw _privateConstructorUsedError; + + Map toJson() => throw _privateConstructorUsedError; + @JsonKey(ignore: true) + $RouterStateCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $RouterStateCopyWith<$Res> { + factory $RouterStateCopyWith( + RouterState value, $Res Function(RouterState) then) = + _$RouterStateCopyWithImpl<$Res, RouterState>; + @useResult + $Res call({bool isInitialized, bool hasAnyAccount, bool hasActiveChat}); +} + +/// @nodoc +class _$RouterStateCopyWithImpl<$Res, $Val extends RouterState> + implements $RouterStateCopyWith<$Res> { + _$RouterStateCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? isInitialized = null, + Object? hasAnyAccount = null, + Object? hasActiveChat = null, + }) { + return _then(_value.copyWith( + isInitialized: null == isInitialized + ? _value.isInitialized + : isInitialized // ignore: cast_nullable_to_non_nullable + as bool, + hasAnyAccount: null == hasAnyAccount + ? _value.hasAnyAccount + : hasAnyAccount // ignore: cast_nullable_to_non_nullable + as bool, + hasActiveChat: null == hasActiveChat + ? _value.hasActiveChat + : hasActiveChat // ignore: cast_nullable_to_non_nullable + as bool, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$RouterStateImplCopyWith<$Res> + implements $RouterStateCopyWith<$Res> { + factory _$$RouterStateImplCopyWith( + _$RouterStateImpl value, $Res Function(_$RouterStateImpl) then) = + __$$RouterStateImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({bool isInitialized, bool hasAnyAccount, bool hasActiveChat}); +} + +/// @nodoc +class __$$RouterStateImplCopyWithImpl<$Res> + extends _$RouterStateCopyWithImpl<$Res, _$RouterStateImpl> + implements _$$RouterStateImplCopyWith<$Res> { + __$$RouterStateImplCopyWithImpl( + _$RouterStateImpl _value, $Res Function(_$RouterStateImpl) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? isInitialized = null, + Object? hasAnyAccount = null, + Object? hasActiveChat = null, + }) { + return _then(_$RouterStateImpl( + isInitialized: null == isInitialized + ? _value.isInitialized + : isInitialized // ignore: cast_nullable_to_non_nullable + as bool, + hasAnyAccount: null == hasAnyAccount + ? _value.hasAnyAccount + : hasAnyAccount // ignore: cast_nullable_to_non_nullable + as bool, + hasActiveChat: null == hasActiveChat + ? _value.hasActiveChat + : hasActiveChat // ignore: cast_nullable_to_non_nullable + as bool, + )); + } +} + +/// @nodoc +@JsonSerializable() +class _$RouterStateImpl implements _RouterState { + const _$RouterStateImpl( + {required this.isInitialized, + required this.hasAnyAccount, + required this.hasActiveChat}); + + factory _$RouterStateImpl.fromJson(Map json) => + _$$RouterStateImplFromJson(json); + + @override + final bool isInitialized; + @override + final bool hasAnyAccount; + @override + final bool hasActiveChat; + + @override + String toString() { + return 'RouterState(isInitialized: $isInitialized, hasAnyAccount: $hasAnyAccount, hasActiveChat: $hasActiveChat)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$RouterStateImpl && + (identical(other.isInitialized, isInitialized) || + other.isInitialized == isInitialized) && + (identical(other.hasAnyAccount, hasAnyAccount) || + other.hasAnyAccount == hasAnyAccount) && + (identical(other.hasActiveChat, hasActiveChat) || + other.hasActiveChat == hasActiveChat)); + } + + @JsonKey(ignore: true) + @override + int get hashCode => + Object.hash(runtimeType, isInitialized, hasAnyAccount, hasActiveChat); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$RouterStateImplCopyWith<_$RouterStateImpl> get copyWith => + __$$RouterStateImplCopyWithImpl<_$RouterStateImpl>(this, _$identity); + + @override + Map toJson() { + return _$$RouterStateImplToJson( + this, + ); + } +} + +abstract class _RouterState implements RouterState { + const factory _RouterState( + {required final bool isInitialized, + required final bool hasAnyAccount, + required final bool hasActiveChat}) = _$RouterStateImpl; + + factory _RouterState.fromJson(Map json) = + _$RouterStateImpl.fromJson; + + @override + bool get isInitialized; + @override + bool get hasAnyAccount; + @override + bool get hasActiveChat; + @override + @JsonKey(ignore: true) + _$$RouterStateImplCopyWith<_$RouterStateImpl> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/lib/router/cubit/router_cubit.g.dart b/lib/router/cubit/router_cubit.g.dart new file mode 100644 index 0000000..f67c770 --- /dev/null +++ b/lib/router/cubit/router_cubit.g.dart @@ -0,0 +1,21 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'router_cubit.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +_$RouterStateImpl _$$RouterStateImplFromJson(Map json) => + _$RouterStateImpl( + isInitialized: json['is_initialized'] as bool, + hasAnyAccount: json['has_any_account'] as bool, + hasActiveChat: json['has_active_chat'] as bool, + ); + +Map _$$RouterStateImplToJson(_$RouterStateImpl instance) => + { + 'is_initialized': instance.isInitialized, + 'has_any_account': instance.hasAnyAccount, + 'has_active_chat': instance.hasActiveChat, + }; diff --git a/lib/router/make_router.dart b/lib/router/make_router.dart deleted file mode 100644 index 37e7a7b..0000000 --- a/lib/router/make_router.dart +++ /dev/null @@ -1,20 +0,0 @@ -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; -import 'package:go_router/go_router.dart'; -import 'package:stream_transform/stream_transform.dart'; - -import '../tools/stream_listenable.dart'; -import 'cubit/router_cubit.dart'; - -final _key = GlobalKey(debugLabel: 'routerKey'); - -/// This simple provider caches our GoRouter. -GoRouter router({required RouterCubit routerCubit}) => GoRouter( - navigatorKey: _key, - refreshListenable: StreamListenable( - routerCubit.stream.startWith(routerCubit.state).distinct()), - debugLogDiagnostics: kDebugMode, - initialLocation: '/', - routes: routerCubit.routes, - redirect: routerCubit.redirect, - ); diff --git a/lib/router/router.dart b/lib/router/router.dart index 5090b83..1867a19 100644 --- a/lib/router/router.dart +++ b/lib/router/router.dart @@ -1,2 +1 @@ export 'cubit/router_cubit.dart'; -export 'make_router.dart'; diff --git a/lib/settings/models/preferences.freezed.dart b/lib/settings/models/preferences.freezed.dart index 3a3e6c7..e9667c8 100644 --- a/lib/settings/models/preferences.freezed.dart +++ b/lib/settings/models/preferences.freezed.dart @@ -12,7 +12,7 @@ part of 'preferences.dart'; T _$identity(T value) => value; final _privateConstructorUsedError = UnsupportedError( - 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#custom-getters-and-methods'); + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); LockPreference _$LockPreferenceFromJson(Map json) { return _LockPreference.fromJson(json); diff --git a/lib/theme/models/theme_preference.freezed.dart b/lib/theme/models/theme_preference.freezed.dart index 3b2b9a4..9f10955 100644 --- a/lib/theme/models/theme_preference.freezed.dart +++ b/lib/theme/models/theme_preference.freezed.dart @@ -12,7 +12,7 @@ part of 'theme_preference.dart'; T _$identity(T value) => value; final _privateConstructorUsedError = UnsupportedError( - 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#custom-getters-and-methods'); + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); ThemePreferences _$ThemePreferencesFromJson(Map json) { return _ThemePreferences.fromJson(json); diff --git a/lib/veilid_processor/models/processor_connection_state.freezed.dart b/lib/veilid_processor/models/processor_connection_state.freezed.dart index a6e01fa..d857318 100644 --- a/lib/veilid_processor/models/processor_connection_state.freezed.dart +++ b/lib/veilid_processor/models/processor_connection_state.freezed.dart @@ -12,7 +12,7 @@ part of 'processor_connection_state.dart'; T _$identity(T value) => value; final _privateConstructorUsedError = UnsupportedError( - 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#custom-getters-and-methods'); + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); /// @nodoc mixin _$ProcessorConnectionState { diff --git a/macos/Podfile.lock b/macos/Podfile.lock index 949cb69..90f42c4 100644 --- a/macos/Podfile.lock +++ b/macos/Podfile.lock @@ -1,9 +1,6 @@ PODS: - FlutterMacOS (1.0.0) - - FMDB (2.7.5): - - FMDB/standard (= 2.7.5) - - FMDB/standard (2.7.5) - - mobile_scanner (3.5.5): + - mobile_scanner (3.5.6): - FlutterMacOS - pasteboard (0.0.1): - FlutterMacOS @@ -19,9 +16,9 @@ PODS: - FlutterMacOS - smart_auth (0.0.1): - FlutterMacOS - - sqflite (0.0.2): + - sqflite (0.0.3): + - Flutter - FlutterMacOS - - FMDB (>= 2.7.5) - url_launcher_macos (0.0.1): - FlutterMacOS - veilid (0.0.1): @@ -38,15 +35,11 @@ DEPENDENCIES: - 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`) + - sqflite (from `Flutter/ephemeral/.symlinks/plugins/sqflite/darwin`) - url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`) - veilid (from `Flutter/ephemeral/.symlinks/plugins/veilid/macos`) - window_manager (from `Flutter/ephemeral/.symlinks/plugins/window_manager/macos`) -SPEC REPOS: - trunk: - - FMDB - EXTERNAL SOURCES: FlutterMacOS: :path: Flutter/ephemeral @@ -65,7 +58,7 @@ EXTERNAL SOURCES: smart_auth: :path: Flutter/ephemeral/.symlinks/plugins/smart_auth/macos sqflite: - :path: Flutter/ephemeral/.symlinks/plugins/sqflite/macos + :path: Flutter/ephemeral/.symlinks/plugins/sqflite/darwin url_launcher_macos: :path: Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos veilid: @@ -75,15 +68,14 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24 - FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a - mobile_scanner: d12930b68bf502497f78b8b5182aeccfaa1e04f6 + mobile_scanner: 54ceceae0c8da2457e26a362a6be5c61154b1829 pasteboard: 9b69dba6fedbb04866be632205d532fe2f6b1d99 - path_provider_foundation: 29f094ae23ebbca9d3d0cec13889cd9060c0e943 + path_provider_foundation: 3784922295ac71e43754bd15e0653ccfd36a147c screen_retriever: 59634572a57080243dd1bf715e55b6c54f241a38 share_plus: 76dd39142738f7a68dd57b05093b5e8193f220f7 - shared_preferences_foundation: 5b919d13b803cadd15ed2dc053125c68730e5126 + shared_preferences_foundation: b4c3b4cddf1c21f02770737f147a3f5da9d39695 smart_auth: b38e3ab4bfe089eacb1e233aca1a2340f96c28e9 - sqflite: a5789cceda41d54d23f31d6de539d65bb14100ea + sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec url_launcher_macos: d2691c7dd33ed713bf3544850a623080ec693d95 veilid: a54f57b7bcf0e4e072fe99272d76ca126b2026d0 window_manager: 3a1844359a6295ab1e47659b1a777e36773cd6e8 diff --git a/packages/async_tools/build.bat b/packages/async_tools/build.bat new file mode 100644 index 0000000..0e2e698 --- /dev/null +++ b/packages/async_tools/build.bat @@ -0,0 +1,2 @@ +@echo off +dart run build_runner build --delete-conflicting-outputs diff --git a/packages/async_tools/build.sh b/packages/async_tools/build.sh new file mode 100755 index 0000000..2a76503 --- /dev/null +++ b/packages/async_tools/build.sh @@ -0,0 +1,3 @@ +#!/bin/bash +set -e +dart run build_runner build --delete-conflicting-outputs diff --git a/packages/async_tools/lib/src/async_value.freezed.dart b/packages/async_tools/lib/src/async_value.freezed.dart index 2632704..b6911e2 100644 --- a/packages/async_tools/lib/src/async_value.freezed.dart +++ b/packages/async_tools/lib/src/async_value.freezed.dart @@ -12,7 +12,7 @@ part of 'async_value.dart'; T _$identity(T value) => value; final _privateConstructorUsedError = UnsupportedError( - 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#custom-getters-and-methods'); + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); /// @nodoc mixin _$AsyncValue { diff --git a/packages/async_tools/pubspec.yaml b/packages/async_tools/pubspec.yaml index a495170..34ae0a4 100644 --- a/packages/async_tools/pubspec.yaml +++ b/packages/async_tools/pubspec.yaml @@ -13,6 +13,7 @@ dependencies: path: ../mutex dev_dependencies: + build_runner: ^2.4.8 freezed: ^2.3.5 lint_hard: ^4.0.0 test: ^1.24.0 diff --git a/packages/veilid_support/lib/proto/proto.dart b/packages/veilid_support/lib/proto/proto.dart index 86f3606..bb861f4 100644 --- a/packages/veilid_support/lib/proto/proto.dart +++ b/packages/veilid_support/lib/proto/proto.dart @@ -100,15 +100,17 @@ extension NonceProto on veilid.Nonce { ..u5 = b.getUint32(5 * 4); return out; } +} - static veilid.Nonce fromProto(proto.Nonce p) { +extension ProtoNonce on proto.Nonce { + veilid.Nonce toVeilid() { final b = ByteData(24) - ..setUint32(0 * 4, p.u0) - ..setUint32(1 * 4, p.u1) - ..setUint32(2 * 4, p.u2) - ..setUint32(3 * 4, p.u3) - ..setUint32(4 * 4, p.u4) - ..setUint32(5 * 4, p.u5); + ..setUint32(0 * 4, u0) + ..setUint32(1 * 4, u1) + ..setUint32(2 * 4, u2) + ..setUint32(3 * 4, u3) + ..setUint32(4 * 4, u4) + ..setUint32(5 * 4, u5); return veilid.Nonce.fromBytes(Uint8List.view(b.buffer)); } } @@ -122,9 +124,11 @@ extension TypedKeyProto on veilid.TypedKey { ..value = value.toProto(); return out; } +} - static veilid.TypedKey fromProto(proto.TypedKey p) => - veilid.TypedKey(kind: p.kind, value: CryptoKeyProto.fromProto(p.value)); +extension ProtoTypedKey on proto.TypedKey { + veilid.TypedKey toVeilid() => + veilid.TypedKey(kind: kind, value: CryptoKeyProto.fromProto(value)); } /// KeyPair protobuf marshaling diff --git a/pubspec.lock b/pubspec.lock index 1d4800b..284c9b7 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -228,10 +228,10 @@ packages: dependency: transitive description: name: camera_platform_interface - sha256: e971ebca970f7cfee396f76ef02070b5e441b4aa04942da9c108d725f57bbd32 + sha256: fceb2c36038b6392317b1d5790c6ba9e6ca9f1da3031181b8bea03882bf9387a url: "https://pub.dev" source: hosted - version: "2.7.2" + version: "2.7.3" camera_web: dependency: transitive description: @@ -449,10 +449,10 @@ packages: dependency: "direct main" description: name: flutter_animate - sha256: cabe33af6201144be052352d53572a1b8a4f5782b46080be7520d95abe763715 + sha256: "7c8a6594a9252dad30cc2ef16e33270b6248c4dedc3b3d06c86c4f3f4dc05ae5" url: "https://pub.dev" source: hosted - version: "4.4.1" + version: "4.5.0" flutter_bloc: dependency: "direct main" description: @@ -481,26 +481,26 @@ packages: dependency: "direct main" description: name: flutter_chat_ui - sha256: "6a4712026429d3b28547bd3d147ded44f8dd53dacc1ff14113693d7b7dd14382" + sha256: c8580c85e2d29359ffc84147e643d08d883eb6e757208652377f0105ef58807f url: "https://pub.dev" source: hosted - version: "1.6.10" + version: "1.6.12" flutter_form_builder: dependency: "direct main" description: name: flutter_form_builder - sha256: e8702c52dc45b43ed42e2b5d9b35f2970096d9cf1a58015cd3a76fad62a8f183 + sha256: "560eb5e367d81170c6ade1e7ae63ecc5167936ae2cdfaae8a345e91bce19d2f2" url: "https://pub.dev" source: hosted - version: "9.2.0" + version: "9.2.1" flutter_hooks: dependency: "direct main" description: name: flutter_hooks - sha256: "09f64db63fee3b2ab8b9038a1346be7d8986977fae3fec601275bf32455ccfc0" + sha256: cde36b12f7188c85286fba9b38cc5a902e7279f36dd676967106c041dc9dde70 url: "https://pub.dev" source: hosted - version: "0.20.4" + version: "0.20.5" flutter_link_previewer: dependency: transitive description: @@ -546,6 +546,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.17" + flutter_shaders: + dependency: transitive + description: + name: flutter_shaders + sha256: "02750b545c01ff4d8e9bbe8f27a7731aa3778402506c67daa1de7f5fc3f4befe" + url: "https://pub.dev" + source: hosted + version: "0.1.2" flutter_slidable: dependency: "direct main" description: @@ -600,10 +608,10 @@ packages: dependency: "direct dev" description: name: freezed - sha256: "6c5031daae12c7072b3a87eff98983076434b4889ef2a44384d0cae3f82372ba" + sha256: "57247f692f35f068cae297549a46a9a097100685c6780fe67177503eea5ed4e5" url: "https://pub.dev" source: hosted - version: "2.4.6" + version: "2.4.7" freezed_annotation: dependency: "direct main" description: @@ -640,10 +648,10 @@ packages: dependency: "direct main" description: name: go_router - sha256: "2ccd74480706e0a70a0e0dfa9543dede41bc11d0fe3b146a6ad7b7686f6b4407" + sha256: "170c46e237d6eb0e6e9f0e8b3f56101e14fb64f787016e42edd74c39cf8b176a" url: "https://pub.dev" source: hosted - version: "11.1.4" + version: "13.2.0" graphs: dependency: transitive description: @@ -712,10 +720,10 @@ packages: dependency: "direct main" description: name: image - sha256: "004a2e90ce080f8627b5a04aecb4cdfac87d2c3f3b520aa291260be5a32c033d" + sha256: "4c68bfd5ae83e700b5204c1e74451e7bf3cf750e6843c6e158289cf56bda018e" url: "https://pub.dev" source: hosted - version: "4.1.4" + version: "4.1.7" intl: dependency: "direct main" description: @@ -824,10 +832,10 @@ packages: dependency: "direct main" description: name: mobile_scanner - sha256: "1b60b8f9d4ce0cb0e7d7bc223c955d083a0737bee66fa1fcfe5de48225e0d5b3" + sha256: "619ed5fd43ca9007a151f00c3dc43feedeaf235fe5647735d0237c38849d49dc" url: "https://pub.dev" source: hosted - version: "3.5.7" + version: "4.0.0" motion_toast: dependency: "direct main" description: @@ -959,10 +967,10 @@ packages: dependency: "direct main" description: name: pinput - sha256: a92b55ecf9c25d1b9e100af45905385d5bc34fc9b6b04177a9e82cb88fe4d805 + sha256: "6d571e38a484f7515a52e89024ef416f11fa6171ac6f32303701374ab9890efa" url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "4.0.0" platform: dependency: transitive description: @@ -1055,10 +1063,10 @@ packages: dependency: "direct main" description: name: qr_code_dart_scan - sha256: ab40c5e8cf09e723fdd1c24ee23ae7187e44d958cc4b1554b4cd094845ae6989 + sha256: ddf409177eeef07f7bdf4e4929858b3acf139873da273315789ebc97bd17bec8 url: "https://pub.dev" source: hosted - version: "0.7.3" + version: "0.7.4" qr_flutter: dependency: "direct main" description: @@ -1236,10 +1244,10 @@ packages: dependency: transitive description: name: smart_auth - sha256: a25229b38c02f733d0a4e98d941b42bed91a976cb589e934895e60ccfa674cf6 + sha256: "88aa8fe66e951c78a307f26d1c29672dce2e9eb3da2e12e853864d0e615a73ad" url: "https://pub.dev" source: hosted - version: "1.1.1" + version: "2.0.0" source_gen: dependency: transitive description: @@ -1272,6 +1280,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.2.1" + sprintf: + dependency: transitive + description: + name: sprintf + sha256: "1fc9ffe69d4df602376b52949af107d8f5703b77cda567c4d7d86a0693120f23" + url: "https://pub.dev" + source: hosted + version: "7.0.0" sqflite: dependency: transitive description: @@ -1468,34 +1484,34 @@ packages: dependency: "direct main" description: name: uuid - sha256: "648e103079f7c64a36dc7d39369cabb358d377078a051d6ae2ad3aa539519313" + sha256: cd210a09f7c18cbe5a02511718e0334de6559871052c90a90c0cca46a4aa81c8 url: "https://pub.dev" source: hosted - version: "3.0.7" + version: "4.3.3" vector_graphics: dependency: transitive description: name: vector_graphics - sha256: "18f6690295af52d081f6808f2f7c69f0eed6d7e23a71539d75f4aeb8f0062172" + sha256: "4ac59808bbfca6da38c99f415ff2d3a5d7ca0a6b4809c71d9cf30fba5daf9752" url: "https://pub.dev" source: hosted - version: "1.1.9+2" + version: "1.1.10+1" vector_graphics_codec: dependency: transitive description: name: vector_graphics_codec - sha256: "531d20465c10dfac7f5cd90b60bbe4dd9921f1ec4ca54c83ebb176dbacb7bb2d" + sha256: f3247e7ab0ec77dc759263e68394990edc608fb2b480b80db8aa86ed09279e33 url: "https://pub.dev" source: hosted - version: "1.1.9+2" + version: "1.1.10+1" vector_graphics_compiler: dependency: transitive description: name: vector_graphics_compiler - sha256: "03012b0a33775c5530576b70240308080e1d5050f0faf000118c20e6463bc0ad" + sha256: "18489bdd8850de3dd7ca8a34e0c446f719ec63e2bab2e7a8cc66a9028dd76c5a" url: "https://pub.dev" source: hosted - version: "1.1.9+2" + version: "1.1.10+1" vector_math: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index f2ed0f5..ebcbef7 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -29,12 +29,12 @@ dependencies: fixnum: ^1.1.0 flutter: sdk: flutter - flutter_animate: ^4.4.1 + flutter_animate: ^4.5.0 flutter_bloc: ^8.1.3 flutter_chat_types: ^3.6.2 - flutter_chat_ui: ^1.6.10 - flutter_form_builder: ^9.2.0 - flutter_hooks: ^0.20.4 + flutter_chat_ui: ^1.6.12 + flutter_form_builder: ^9.2.1 + flutter_hooks: ^0.20.5 flutter_localizations: sdk: flutter flutter_native_splash: ^2.3.10 @@ -44,25 +44,25 @@ dependencies: flutter_translate: ^4.0.4 form_builder_validators: ^9.1.0 freezed_annotation: ^2.4.1 - go_router: ^11.1.4 + go_router: ^13.2.0 hydrated_bloc: ^9.1.3 - image: ^4.1.4 + image: ^4.1.7 intl: ^0.18.1 json_annotation: ^4.8.1 loggy: ^2.0.3 meta: ^1.10.0 - mobile_scanner: ^3.5.7 + mobile_scanner: ^4.0.0 motion_toast: ^2.8.0 mutex: path: packages/mutex pasteboard: ^0.2.0 path: ^1.8.3 path_provider: ^2.1.2 - pinput: ^3.0.1 + pinput: ^4.0.0 preload_page_view: ^0.2.0 protobuf: ^3.1.0 provider: ^6.1.1 - qr_code_dart_scan: ^0.7.3 + qr_code_dart_scan: ^0.7.4 qr_flutter: ^4.1.0 quickalert: ^1.0.2 radix_colors: ^1.0.4 @@ -75,7 +75,7 @@ dependencies: stack_trace: ^1.11.1 stream_transform: ^2.1.0 stylish_bottom_bar: ^1.0.3 - uuid: ^3.0.7 + uuid: ^4.3.3 veilid: # veilid: ^0.0.1 path: ../veilid/veilid-flutter @@ -89,7 +89,7 @@ dev_dependencies: build_runner: ^2.4.8 flutter_test: sdk: flutter - freezed: ^2.4.6 + freezed: ^2.4.7 icons_launcher: ^2.1.7 json_serializable: ^6.7.1 lint_hard: ^4.0.0 From 9dd17cb0770d6a7cbfc037a5d1f2c1a67970db09 Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Mon, 12 Feb 2024 09:10:07 -0500 Subject: [PATCH 34/68] checkpoint --- lib/chat/views/chat_component.dart | 4 +- .../active_conversation_messages_cubit.dart | 15 +++-- lib/chat_list/cubits/chat_list_cubit.dart | 3 +- .../chat_single_contact_item_widget.dart | 2 +- .../cubits/contact_invitation_list_cubit.dart | 47 ++++++------- .../models/valid_contact_invitation.dart | 4 +- lib/contacts/cubits/contact_list_cubit.dart | 8 +-- lib/contacts/views/contact_item_widget.dart | 2 +- .../lib/dht_support/proto/proto.dart | 10 +-- .../lib/dht_support/src/dht_short_array.dart | 3 +- packages/veilid_support/lib/proto/proto.dart | 67 ++++++++++--------- 11 files changed, 80 insertions(+), 85 deletions(-) diff --git a/lib/chat/views/chat_component.dart b/lib/chat/views/chat_component.dart index 0e3111a..ba866c4 100644 --- a/lib/chat/views/chat_component.dart +++ b/lib/chat/views/chat_component.dart @@ -74,9 +74,7 @@ class ChatComponent extends StatelessWidget { ); final editedName = conversation.contact.editedProfile.name; final remoteUser = types.User( - id: proto.TypedKeyProto.fromProto( - conversation.contact.identityPublicKey) - .toString(), + id: conversation.contact.identityPublicKey.toVeilid().toString(), firstName: editedName); // Get the messages to display diff --git a/lib/chat_list/cubits/active_conversation_messages_cubit.dart b/lib/chat_list/cubits/active_conversation_messages_cubit.dart index 032aebc..bb6209c 100644 --- a/lib/chat_list/cubits/active_conversation_messages_cubit.dart +++ b/lib/chat_list/cubits/active_conversation_messages_cubit.dart @@ -91,14 +91,17 @@ class ActiveConversationMessagesCubit extends BlocMapCubit add(() => MapEntry( - proto.TypedKeyProto.fromProto(contact.remoteConversationRecordKey), + contact.remoteConversationRecordKey.toVeilid(), MessagesCubit( activeAccountInfo: _activeAccountInfo, - remoteIdentityPublicKey: contact.identityPublicKey, - localConversationRecordKey: contact.localConversationRecordKey, - remoteConversationRecordKey: contact.remoteConversationRecordKey, - localMessagesRecordKey: localConversation.messages, - remoteMessagesRecordKey: remoteConversation.messages))); + remoteIdentityPublicKey: contact.identityPublicKey.toVeilid(), + localConversationRecordKey: + contact.localConversationRecordKey.toVeilid(), + remoteConversationRecordKey: + contact.remoteConversationRecordKey.toVeilid(), + localMessagesRecordKey: localConversation.messages.toVeilid(), + remoteMessagesRecordKey: + remoteConversation.messages.toVeilid()))); //// diff --git a/lib/chat_list/cubits/chat_list_cubit.dart b/lib/chat_list/cubits/chat_list_cubit.dart index bd9b9c5..f0b85ee 100644 --- a/lib/chat_list/cubits/chat_list_cubit.dart +++ b/lib/chat_list/cubits/chat_list_cubit.dart @@ -23,8 +23,7 @@ class ChatListCubit extends DHTShortArrayCubit { final accountRecordKey = activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey; - final chatListRecordKey = - proto.OwnedDHTRecordPointerProto.fromProto(account.chatList); + final chatListRecordKey = account.chatList.toVeilid(); final dhtRecord = await DHTShortArray.openOwned(chatListRecordKey, parent: accountRecordKey); diff --git a/lib/chat_list/views/chat_single_contact_item_widget.dart b/lib/chat_list/views/chat_single_contact_item_widget.dart index ee3c273..64f5a1f 100644 --- a/lib/chat_list/views/chat_single_contact_item_widget.dart +++ b/lib/chat_list/views/chat_single_contact_item_widget.dart @@ -26,7 +26,7 @@ class ChatSingleContactItemWidget extends StatelessWidget { final activeChatCubit = context.watch(); final remoteConversationRecordKey = - proto.TypedKeyProto.fromProto(_contact.remoteConversationRecordKey); + _contact.remoteConversationRecordKey.toVeilid(); final selected = activeChatCubit.state == remoteConversationRecordKey; return Container( diff --git a/lib/contact_invitation/cubits/contact_invitation_list_cubit.dart b/lib/contact_invitation/cubits/contact_invitation_list_cubit.dart index 6ff1c8a..9c484a7 100644 --- a/lib/contact_invitation/cubits/contact_invitation_list_cubit.dart +++ b/lib/contact_invitation/cubits/contact_invitation_list_cubit.dart @@ -50,8 +50,7 @@ class ContactInvitationListCubit activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey; final contactInvitationListRecordKey = - proto.OwnedDHTRecordPointerProto.fromProto( - account.contactInvitationRecords); + account.contactInvitationRecords.toVeilid(); final dhtRecord = await DHTShortArray.openOwned( contactInvitationListRecordKey, @@ -175,8 +174,7 @@ class ContactInvitationListCubit } } await (await pool.openOwned( - proto.OwnedDHTRecordPointerProto.fromProto( - contactInvitationRecord.contactRequestInbox), + contactInvitationRecord.contactRequestInbox.toVeilid(), parent: accountRecordKey)) .scope((contactRequestInbox) async { // Wipe out old invitation so it shows up as invalid @@ -185,8 +183,7 @@ class ContactInvitationListCubit }); if (!accepted) { await (await pool.openRead( - proto.TypedKeyProto.fromProto( - contactInvitationRecord.localConversationRecordKey), + contactInvitationRecord.localConversationRecordKey.toVeilid(), parent: accountRecordKey)) .delete(); } @@ -205,7 +202,7 @@ class ContactInvitationListCubit final contactInvitation = proto.ContactInvitation.fromBuffer(contactInvitationBytes); final contactRequestInboxKey = - proto.TypedKeyProto.fromProto(contactInvitation.contactRequestInboxKey); + contactInvitation.contactRequestInboxKey.toVeilid(); ValidContactInvitation? out; @@ -216,7 +213,7 @@ class ContactInvitationListCubit // If we're chatting to ourselves, // we are validating an invitation we have created final isSelf = state.data!.value.indexWhere((cir) => - proto.TypedKeyProto.fromProto(cir.contactRequestInbox.recordKey) == + cir.contactRequestInbox.recordKey.toVeilid() == contactRequestInboxKey) != -1; @@ -246,21 +243,20 @@ class ContactInvitationListCubit final contactRequestPrivate = proto.ContactRequestPrivate.fromBuffer(contactRequestPrivateBytes); - final contactIdentityMasterRecordKey = proto.TypedKeyProto.fromProto( - contactRequestPrivate.identityMasterRecordKey); + final contactIdentityMasterRecordKey = + contactRequestPrivate.identityMasterRecordKey.toVeilid(); // Fetch the account master final contactIdentityMaster = await openIdentityMaster( identityMasterRecordKey: contactIdentityMasterRecordKey); // Verify - final signature = proto.SignatureProto.fromProto( - signedContactInvitation.identitySignature); + final signature = signedContactInvitation.identitySignature.toVeilid(); await cs.verify(contactIdentityMaster.identityPublicKey, contactInvitationBytes, signature); final writer = KeyPair( - key: proto.CryptoKeyProto.fromProto(contactRequestPrivate.writerKey), + key: contactRequestPrivate.writerKey.toVeilid(), secret: writerSecret); out = ValidContactInvitation( @@ -282,12 +278,10 @@ class ContactInvitationListCubit final pool = DHTRecordPool.instance; final accountRecordKey = _activeAccountInfo .userLogin.accountRecordInfo.accountRecord.recordKey; - final writerKey = - proto.CryptoKeyProto.fromProto(contactInvitationRecord.writerKey); - final writerSecret = - proto.CryptoKeyProto.fromProto(contactInvitationRecord.writerSecret); - final recordKey = proto.TypedKeyProto.fromProto( - contactInvitationRecord.contactRequestInbox.recordKey); + final writerKey = contactInvitationRecord.writerKey.toVeilid(); + final writerSecret = contactInvitationRecord.writerSecret.toVeilid(); + final recordKey = + contactInvitationRecord.contactRequestInbox.recordKey.toVeilid(); final writer = TypedKeyPair( kind: recordKey.kind, key: writerKey, secret: writerSecret); final acceptReject = await (await pool.openRead(recordKey, @@ -307,8 +301,8 @@ class ContactInvitationListCubit Uint8List.fromList(signedContactResponse.contactResponse); final contactResponse = proto.ContactResponse.fromBuffer(contactResponseBytes); - final contactIdentityMasterRecordKey = proto.TypedKeyProto.fromProto( - contactResponse.identityMasterRecordKey); + final contactIdentityMasterRecordKey = + contactResponse.identityMasterRecordKey.toVeilid(); final cs = await pool.veilid.getCryptoSystem(recordKey.kind); // Fetch the remote contact's account master @@ -316,8 +310,7 @@ class ContactInvitationListCubit identityMasterRecordKey: contactIdentityMasterRecordKey); // Verify - final signature = proto.SignatureProto.fromProto( - signedContactResponse.identitySignature); + final signature = signedContactResponse.identitySignature.toVeilid(); await cs.verify(contactIdentityMaster.identityPublicKey, contactResponseBytes, signature); @@ -327,8 +320,8 @@ class ContactInvitationListCubit } // Pull profile from remote conversation key - final remoteConversationRecordKey = proto.TypedKeyProto.fromProto( - contactResponse.remoteConversationRecordKey); + final remoteConversationRecordKey = + contactResponse.remoteConversationRecordKey.toVeilid(); final conversation = ConversationCubit( activeAccountInfo: _activeAccountInfo, @@ -345,8 +338,8 @@ class ContactInvitationListCubit } // Complete the local conversation now that we have the remote profile - final localConversationRecordKey = proto.TypedKeyProto.fromProto( - contactInvitationRecord.localConversationRecordKey); + final localConversationRecordKey = + contactInvitationRecord.localConversationRecordKey.toVeilid(); return conversation.initLocalConversation( existingConversationRecordKey: localConversationRecordKey, profile: _account.profile, diff --git a/lib/contact_invitation/models/valid_contact_invitation.dart b/lib/contact_invitation/models/valid_contact_invitation.dart index 67e509a..d4db157 100644 --- a/lib/contact_invitation/models/valid_contact_invitation.dart +++ b/lib/contact_invitation/models/valid_contact_invitation.dart @@ -80,8 +80,8 @@ class ValidContactInvitation { return AcceptedContact( remoteProfile: _contactRequestPrivate.profile, remoteIdentity: _contactIdentityMaster, - remoteConversationRecordKey: proto.TypedKeyProto.fromProto( - _contactRequestPrivate.chatRecordKey), + remoteConversationRecordKey: + _contactRequestPrivate.chatRecordKey.toVeilid(), localConversationRecordKey: localConversation.key, ); }); diff --git a/lib/contacts/cubits/contact_list_cubit.dart b/lib/contacts/cubits/contact_list_cubit.dart index 004dd51..c2f5200 100644 --- a/lib/contacts/cubits/contact_list_cubit.dart +++ b/lib/contacts/cubits/contact_list_cubit.dart @@ -24,8 +24,7 @@ class ContactListCubit extends DHTShortArrayCubit { final accountRecordKey = activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey; - final contactListRecordKey = - proto.OwnedDHTRecordPointerProto.fromProto(account.contactList); + final contactListRecordKey = account.contactList.toVeilid(); final dhtRecord = await DHTShortArray.openOwned(contactListRecordKey, parent: accountRecordKey); @@ -63,10 +62,9 @@ class ContactListCubit extends DHTShortArrayCubit { final pool = DHTRecordPool.instance; final accountRecordKey = _activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey; - final localConversationKey = - proto.TypedKeyProto.fromProto(contact.localConversationRecordKey); + final localConversationKey = contact.localConversationRecordKey.toVeilid(); final remoteConversationKey = - proto.TypedKeyProto.fromProto(contact.remoteConversationRecordKey); + contact.remoteConversationRecordKey.toVeilid(); // Remove Contact from account's list for (var i = 0; i < shortArray.length; i++) { diff --git a/lib/contacts/views/contact_item_widget.dart b/lib/contacts/views/contact_item_widget.dart index 52cfb42..8823150 100644 --- a/lib/contacts/views/contact_item_widget.dart +++ b/lib/contacts/views/contact_item_widget.dart @@ -25,7 +25,7 @@ class ContactItemWidget extends StatelessWidget { final scale = theme.extension()!; final remoteConversationKey = - proto.TypedKeyProto.fromProto(contact.remoteConversationRecordKey); + contact.remoteConversationRecordKey.toVeilid(); return Container( margin: const EdgeInsets.fromLTRB(0, 4, 0, 0), diff --git a/packages/veilid_support/lib/dht_support/proto/proto.dart b/packages/veilid_support/lib/dht_support/proto/proto.dart index f61e342..6b36970 100644 --- a/packages/veilid_support/lib/dht_support/proto/proto.dart +++ b/packages/veilid_support/lib/dht_support/proto/proto.dart @@ -17,9 +17,9 @@ extension OwnedDHTRecordPointerProto on OwnedDHTRecordPointer { ..owner = owner.toProto(); return out; } - - static OwnedDHTRecordPointer fromProto(dhtproto.OwnedDHTRecordPointer p) => - OwnedDHTRecordPointer( - recordKey: veilidproto.TypedKeyProto.fromProto(p.recordKey), - owner: veilidproto.KeyPairProto.fromProto(p.owner)); +} + +extension ProtoOwnedDHTRecordPointer on dhtproto.OwnedDHTRecordPointer { + OwnedDHTRecordPointer toVeilid() => OwnedDHTRecordPointer( + recordKey: recordKey.toVeilid(), owner: owner.toVeilid()); } diff --git a/packages/veilid_support/lib/dht_support/src/dht_short_array.dart b/packages/veilid_support/lib/dht_support/src/dht_short_array.dart index 40d8b90..1f7142e 100644 --- a/packages/veilid_support/lib/dht_support/src/dht_short_array.dart +++ b/packages/veilid_support/lib/dht_support/src/dht_short_array.dart @@ -241,8 +241,7 @@ class DHTShortArray { /// Validate a new head record Future _newHead(proto.DHTShortArray head) async { // Get the set of new linked keys and validate it - final linkedKeys = - head.keys.map(proto.TypedKeyProto.fromProto).toList(); + final linkedKeys = head.keys.map((p) => p.toVeilid()).toList(); final index = head.index; final free = _validateHeadCacheData(linkedKeys, index); diff --git a/packages/veilid_support/lib/proto/proto.dart b/packages/veilid_support/lib/proto/proto.dart index bb861f4..a7a70bb 100644 --- a/packages/veilid_support/lib/proto/proto.dart +++ b/packages/veilid_support/lib/proto/proto.dart @@ -24,17 +24,19 @@ extension CryptoKeyProto on veilid.CryptoKey { ..u7 = b.getUint32(7 * 4); return out; } +} - static veilid.CryptoKey fromProto(proto.CryptoKey p) { +extension ProtoCryptoKey on proto.CryptoKey { + veilid.CryptoKey toVeilid() { final b = ByteData(32) - ..setUint32(0 * 4, p.u0) - ..setUint32(1 * 4, p.u1) - ..setUint32(2 * 4, p.u2) - ..setUint32(3 * 4, p.u3) - ..setUint32(4 * 4, p.u4) - ..setUint32(5 * 4, p.u5) - ..setUint32(6 * 4, p.u6) - ..setUint32(7 * 4, p.u7); + ..setUint32(0 * 4, u0) + ..setUint32(1 * 4, u1) + ..setUint32(2 * 4, u2) + ..setUint32(3 * 4, u3) + ..setUint32(4 * 4, u4) + ..setUint32(5 * 4, u5) + ..setUint32(6 * 4, u6) + ..setUint32(7 * 4, u7); return veilid.CryptoKey.fromBytes(Uint8List.view(b.buffer)); } } @@ -63,25 +65,27 @@ extension SignatureProto on veilid.Signature { ..u15 = b.getUint32(15 * 4); return out; } +} - static veilid.Signature fromProto(proto.Signature p) { +extension ProtoSignature on proto.Signature { + veilid.Signature toVeilid() { final b = ByteData(64) - ..setUint32(0 * 4, p.u0) - ..setUint32(1 * 4, p.u1) - ..setUint32(2 * 4, p.u2) - ..setUint32(3 * 4, p.u3) - ..setUint32(4 * 4, p.u4) - ..setUint32(5 * 4, p.u5) - ..setUint32(6 * 4, p.u6) - ..setUint32(7 * 4, p.u7) - ..setUint32(8 * 4, p.u8) - ..setUint32(9 * 4, p.u9) - ..setUint32(10 * 4, p.u10) - ..setUint32(11 * 4, p.u11) - ..setUint32(12 * 4, p.u12) - ..setUint32(13 * 4, p.u13) - ..setUint32(14 * 4, p.u14) - ..setUint32(15 * 4, p.u15); + ..setUint32(0 * 4, u0) + ..setUint32(1 * 4, u1) + ..setUint32(2 * 4, u2) + ..setUint32(3 * 4, u3) + ..setUint32(4 * 4, u4) + ..setUint32(5 * 4, u5) + ..setUint32(6 * 4, u6) + ..setUint32(7 * 4, u7) + ..setUint32(8 * 4, u8) + ..setUint32(9 * 4, u9) + ..setUint32(10 * 4, u10) + ..setUint32(11 * 4, u11) + ..setUint32(12 * 4, u12) + ..setUint32(13 * 4, u13) + ..setUint32(14 * 4, u14) + ..setUint32(15 * 4, u15); return veilid.Signature.fromBytes(Uint8List.view(b.buffer)); } } @@ -128,7 +132,7 @@ extension TypedKeyProto on veilid.TypedKey { extension ProtoTypedKey on proto.TypedKey { veilid.TypedKey toVeilid() => - veilid.TypedKey(kind: kind, value: CryptoKeyProto.fromProto(value)); + veilid.TypedKey(kind: kind, value: value.toVeilid()); } /// KeyPair protobuf marshaling @@ -140,8 +144,9 @@ extension KeyPairProto on veilid.KeyPair { ..secret = secret.toProto(); return out; } - - static veilid.KeyPair fromProto(proto.KeyPair p) => veilid.KeyPair( - key: CryptoKeyProto.fromProto(p.key), - secret: CryptoKeyProto.fromProto(p.secret)); +} + +extension ProtoKeyPair on proto.KeyPair { + veilid.KeyPair toVeilid() => + veilid.KeyPair(key: key.toVeilid(), secret: secret.toVeilid()); } From a551791f97e02021c778ca6ae1ea32e3725ce14d Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Mon, 12 Feb 2024 10:35:41 -0500 Subject: [PATCH 35/68] cleanup --- devtools_options.yaml | 1 + lib/app.dart | 4 ++++ lib/init.dart | 6 +++--- lib/tools/state_logger.dart | 35 +++++++++++++++++++++++++++++++---- 4 files changed, 39 insertions(+), 7 deletions(-) create mode 100644 devtools_options.yaml diff --git a/devtools_options.yaml b/devtools_options.yaml new file mode 100644 index 0000000..7e7e7f6 --- /dev/null +++ b/devtools_options.yaml @@ -0,0 +1 @@ +extensions: diff --git a/lib/app.dart b/lib/app.dart index 44802bd..3cb2093 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -10,6 +10,7 @@ import 'account_manager/account_manager.dart'; import 'router/router.dart'; import 'settings/settings.dart'; import 'tick.dart'; +import 'veilid_processor/veilid_processor.dart'; class VeilidChatApp extends StatelessWidget { const VeilidChatApp({ @@ -31,6 +32,9 @@ class VeilidChatApp extends StatelessWidget { state: LocalizationProvider.of(context).state, child: MultiBlocProvider( providers: [ + BlocProvider( + create: (context) => + ConnectionStateCubit(ProcessorRepository.instance)), BlocProvider( create: (context) => RouterCubit(AccountRepository.instance), diff --git a/lib/init.dart b/lib/init.dart index 29c2db2..edc954b 100644 --- a/lib/init.dart +++ b/lib/init.dart @@ -19,11 +19,11 @@ Future initializeVeilid() async { // Veilid logging initVeilidLog(kDebugMode); - // DHT Record Pool - await DHTRecordPool.init(); - // Startup Veilid await ProcessorRepository.instance.startup(); + + // DHT Record Pool + await DHTRecordPool.init(); } // Initialize repositories diff --git a/lib/tools/state_logger.dart b/lib/tools/state_logger.dart index 28230a8..0f27504 100644 --- a/lib/tools/state_logger.dart +++ b/lib/tools/state_logger.dart @@ -1,33 +1,60 @@ import 'package:bloc/bloc.dart'; +import 'package:loggy/loggy.dart'; import 'loggy.dart'; +const Map _blocChangeLogLevels = { + 'ConnectionStateCubit': LogLevel.off +}; +const Map _blocCreateCloseLogLevels = {}; +const Map _blocErrorLogLevels = {}; + /// [BlocObserver] for the VeilidChat application that /// observes all state changes. class StateLogger extends BlocObserver { /// {@macro counter_observer} const StateLogger(); + void _checkLogLevel( + Map blocLogLevels, + LogLevel defaultLogLevel, + BlocBase bloc, + void Function(LogLevel) closure) { + final logLevel = + blocLogLevels[bloc.runtimeType.toString()] ?? defaultLogLevel; + if (logLevel != LogLevel.off) { + closure(logLevel); + } + } + @override void onChange(BlocBase bloc, Change change) { super.onChange(bloc, change); - log.debug('Change: ${bloc.runtimeType} $change'); + _checkLogLevel(_blocChangeLogLevels, LogLevel.debug, bloc, (logLevel) { + log.log(logLevel, 'Change: ${bloc.runtimeType} $change'); + }); } @override void onCreate(BlocBase bloc) { super.onCreate(bloc); - log.debug('Create: ${bloc.runtimeType}'); + _checkLogLevel(_blocCreateCloseLogLevels, LogLevel.debug, bloc, (logLevel) { + log.log(logLevel, 'Create: ${bloc.runtimeType}'); + }); } @override void onClose(BlocBase bloc) { super.onClose(bloc); - log.debug('Close: ${bloc.runtimeType}'); + _checkLogLevel(_blocCreateCloseLogLevels, LogLevel.debug, bloc, (logLevel) { + log.log(logLevel, 'Close: ${bloc.runtimeType}'); + }); } @override void onError(BlocBase bloc, Object error, StackTrace stackTrace) { super.onError(bloc, error, stackTrace); - log.error('Error: ${bloc.runtimeType} $error\n$stackTrace'); + _checkLogLevel(_blocErrorLogLevels, LogLevel.error, bloc, (logLevel) { + log.log(logLevel, 'Error: ${bloc.runtimeType} $error\n$stackTrace'); + }); } } From 45ab4949696c2e6f2c41a32f6210f4813126114a Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Mon, 12 Feb 2024 22:30:09 -0500 Subject: [PATCH 36/68] things compile --- lib/app.dart | 3 +- lib/router/cubit/router_cubit.dart | 44 +++++++++++++++---- lib/settings/models/preferences.dart | 4 +- lib/theme/views/color_preferences.dart | 2 - lib/tools/widget_helpers.dart | 2 +- .../lib/dht_support/src/dht_record_pool.dart | 2 + 6 files changed, 42 insertions(+), 15 deletions(-) diff --git a/lib/app.dart b/lib/app.dart index 3cb2093..e7569c0 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -59,8 +59,7 @@ class VeilidChatApp extends StatelessWidget { child: BackgroundTicker( builder: (context) => MaterialApp.router( debugShowCheckedModeBanner: false, - routerConfig: - BlocProvider.of(context).router(), + routerConfig: context.watch().router(), title: translate('app.title'), theme: theme, localizationsDelegates: [ diff --git a/lib/router/cubit/router_cubit.dart b/lib/router/cubit/router_cubit.dart index b0ba3eb..aedae69 100644 --- a/lib/router/cubit/router_cubit.dart +++ b/lib/router/cubit/router_cubit.dart @@ -127,6 +127,9 @@ class RouterCubit extends Cubit { if (!state.hasAnyAccount) { return '/new_account'; } + if (!state.hasActiveChat) { xxx stop using hasActiveChat here... we need a pager for the accounts and a way to get the current account state maybe a 'activeAccountCubit' or something, we may have this alraeady but it needs to work even if logged out.`` + return '/home/no_active'; + } if (responsiveVisibility( context: context, tablet: false, @@ -141,6 +144,9 @@ class RouterCubit extends Cubit { if (!state.hasAnyAccount) { return '/new_account'; } + if (!state.hasActiveChat) { + return '/home/no_active'; + } if (responsiveVisibility( context: context, tablet: false, @@ -153,6 +159,21 @@ class RouterCubit extends Cubit { return '/home'; } return null; + case '/home/no_active': + if (state.hasActiveChat) { + return '/home'; + } + return null; + case '/home/account_missing': + if (!state.hasActiveChat) { + return '/home/no_active'; + } + return null; + case '/home/account_locked': + if (!state.hasActiveChat) { + return '/home/no_active'; + } + return null; case '/settings': return null; case '/developer': @@ -163,17 +184,24 @@ class RouterCubit extends Cubit { } /// Make a GoRouter instance that uses this cubit - GoRouter router() => GoRouter( - navigatorKey: _rootNavKey, - refreshListenable: StreamListenable(stream.startWith(state).distinct()), - debugLogDiagnostics: kDebugMode, - initialLocation: '/', - routes: routes, - redirect: redirect, - ); + GoRouter router() { + final r = _router; + if (r != null) { + return r; + } + return _router = GoRouter( + navigatorKey: _rootNavKey, + refreshListenable: StreamListenable(stream.startWith(state).distinct()), + debugLogDiagnostics: kDebugMode, + initialLocation: '/', + routes: routes, + redirect: redirect, + ); + } //////////////// late final StreamSubscription _accountRepositorySubscription; + GoRouter? _router; } diff --git a/lib/settings/models/preferences.dart b/lib/settings/models/preferences.dart index 8ff626f..8dfcb73 100644 --- a/lib/settings/models/preferences.dart +++ b/lib/settings/models/preferences.dart @@ -28,13 +28,13 @@ class LockPreference with _$LockPreference { // Theme supports multiple translations enum LanguagePreference { - englishUS; + englishUs; factory LanguagePreference.fromJson(dynamic j) => LanguagePreference.values.byName((j as String).toCamelCase()); String toJson() => name.toPascalCase(); - static const LanguagePreference defaults = LanguagePreference.englishUS; + static const LanguagePreference defaults = LanguagePreference.englishUs; } // Preferences are stored in a table locally and globally affect all diff --git a/lib/theme/views/color_preferences.dart b/lib/theme/views/color_preferences.dart index 67e91f0..a364e00 100644 --- a/lib/theme/views/color_preferences.dart +++ b/lib/theme/views/color_preferences.dart @@ -49,7 +49,5 @@ Widget buildSettingsPageColorPreferences({required void Function() onChanged}) { await preferencesRepository.set(newPrefs); switcher.changeTheme(theme: newThemePrefs.themeData()); onChanged(); - - onChanged(); })); } diff --git a/lib/tools/widget_helpers.dart b/lib/tools/widget_helpers.dart index c9ffa41..60ef937 100644 --- a/lib/tools/widget_helpers.dart +++ b/lib/tools/widget_helpers.dart @@ -44,7 +44,7 @@ Widget waitingPage({String? text}) => Builder( color: Theme.of(context).scaffoldBackgroundColor, child: Center( child: Column(children: [ - buildProgressIndicator(), + buildProgressIndicator().expanded(), if (text != null) Text(text) ])))); diff --git a/packages/veilid_support/lib/dht_support/src/dht_record_pool.dart b/packages/veilid_support/lib/dht_support/src/dht_record_pool.dart index ee4b426..208ac98 100644 --- a/packages/veilid_support/lib/dht_support/src/dht_record_pool.dart +++ b/packages/veilid_support/lib/dht_support/src/dht_record_pool.dart @@ -230,6 +230,8 @@ class DHTRecordPool with TableDBBacked { final dhtctx = routingContext ?? _routingContext; final recordDescriptor = await dhtctx.createDHTRecord(schema); + await _locks.lockTag(recordDescriptor.key); + final rec = DHTRecord( routingContext: dhtctx, recordDescriptor: recordDescriptor, From 9219e1307e4e3a58c25f67debf1543a286468331 Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Tue, 13 Feb 2024 22:03:26 -0500 Subject: [PATCH 37/68] more refactor --- lib/account_manager/account_manager.dart | 2 +- .../active_user_login_state.dart | 3 - lib/account_manager/cubit/cubit.dart | 4 - .../local_accounts_state.dart | 14 -- .../user_logins_cubit/user_logins_state.dart | 14 -- .../account_record_cubit.dart | 0 .../active_local_account_cubit.dart} | 12 +- lib/account_manager/cubits/cubits.dart | 4 + .../local_accounts_cubit.dart | 13 +- .../user_logins_cubit.dart | 13 +- .../account_repository.dart | 170 ++++++++-------- .../account_repository/active_logins.dart | 25 --- .../active_logins.freezed.dart | 181 ------------------ .../account_repository/active_logins.g.dart | 24 --- lib/app.dart | 4 +- lib/chat/cubits/active_chat_cubit.dart | 5 +- .../home_account_ready_shell.dart | 49 ++--- lib/layout/home/home_shell.dart | 37 +++- lib/router/cubit/router_cubit.dart | 74 ++----- 19 files changed, 181 insertions(+), 467 deletions(-) delete mode 100644 lib/account_manager/cubit/active_user_login_cubit/active_user_login_state.dart delete mode 100644 lib/account_manager/cubit/cubit.dart delete mode 100644 lib/account_manager/cubit/local_accounts_cubit/local_accounts_state.dart delete mode 100644 lib/account_manager/cubit/user_logins_cubit/user_logins_state.dart rename lib/account_manager/{cubit => cubits}/account_record_cubit.dart (100%) rename lib/account_manager/{cubit/active_user_login_cubit/active_user_login_cubit.dart => cubits/active_local_account_cubit.dart} (71%) create mode 100644 lib/account_manager/cubits/cubits.dart rename lib/account_manager/{cubit/local_accounts_cubit => cubits}/local_accounts_cubit.dart (73%) rename lib/account_manager/{cubit/user_logins_cubit => cubits}/user_logins_cubit.dart (74%) delete mode 100644 lib/account_manager/repository/account_repository/active_logins.dart delete mode 100644 lib/account_manager/repository/account_repository/active_logins.freezed.dart delete mode 100644 lib/account_manager/repository/account_repository/active_logins.g.dart diff --git a/lib/account_manager/account_manager.dart b/lib/account_manager/account_manager.dart index af728ac..ac518b3 100644 --- a/lib/account_manager/account_manager.dart +++ b/lib/account_manager/account_manager.dart @@ -1,4 +1,4 @@ -export 'cubit/cubit.dart'; +export 'cubits/cubits.dart'; export 'models/models.dart'; export 'repository/repository.dart'; export 'views/views.dart'; diff --git a/lib/account_manager/cubit/active_user_login_cubit/active_user_login_state.dart b/lib/account_manager/cubit/active_user_login_cubit/active_user_login_state.dart deleted file mode 100644 index 098e7a4..0000000 --- a/lib/account_manager/cubit/active_user_login_cubit/active_user_login_state.dart +++ /dev/null @@ -1,3 +0,0 @@ -part of 'active_user_login_cubit.dart'; - -typedef ActiveUserLoginState = TypedKey?; diff --git a/lib/account_manager/cubit/cubit.dart b/lib/account_manager/cubit/cubit.dart deleted file mode 100644 index d86274b..0000000 --- a/lib/account_manager/cubit/cubit.dart +++ /dev/null @@ -1,4 +0,0 @@ -export 'account_record_cubit.dart'; -export 'active_user_login_cubit/active_user_login_cubit.dart'; -export 'local_accounts_cubit/local_accounts_cubit.dart'; -export 'user_logins_cubit/user_logins_cubit.dart'; diff --git a/lib/account_manager/cubit/local_accounts_cubit/local_accounts_state.dart b/lib/account_manager/cubit/local_accounts_cubit/local_accounts_state.dart deleted file mode 100644 index 3b8d695..0000000 --- a/lib/account_manager/cubit/local_accounts_cubit/local_accounts_state.dart +++ /dev/null @@ -1,14 +0,0 @@ -part of 'local_accounts_cubit.dart'; - -typedef LocalAccountsState = IList; - -extension LocalAccountsStateExt on LocalAccountsState { - LocalAccount? fetchLocalAccount({required TypedKey accountMasterRecordKey}) { - final idx = indexWhere( - (e) => e.identityMaster.masterRecordKey == accountMasterRecordKey); - if (idx == -1) { - return null; - } - return this[idx]; - } -} diff --git a/lib/account_manager/cubit/user_logins_cubit/user_logins_state.dart b/lib/account_manager/cubit/user_logins_cubit/user_logins_state.dart deleted file mode 100644 index 04c70d7..0000000 --- a/lib/account_manager/cubit/user_logins_cubit/user_logins_state.dart +++ /dev/null @@ -1,14 +0,0 @@ -part of 'user_logins_cubit.dart'; - -typedef UserLoginsState = IList; - -extension UserLoginsStateExt on UserLoginsState { - UserLogin? fetchUserLogin({required TypedKey accountMasterRecordKey}) { - final idx = - indexWhere((e) => e.accountMasterRecordKey == accountMasterRecordKey); - if (idx == -1) { - return null; - } - return this[idx]; - } -} diff --git a/lib/account_manager/cubit/account_record_cubit.dart b/lib/account_manager/cubits/account_record_cubit.dart similarity index 100% rename from lib/account_manager/cubit/account_record_cubit.dart rename to lib/account_manager/cubits/account_record_cubit.dart diff --git a/lib/account_manager/cubit/active_user_login_cubit/active_user_login_cubit.dart b/lib/account_manager/cubits/active_local_account_cubit.dart similarity index 71% rename from lib/account_manager/cubit/active_user_login_cubit/active_user_login_cubit.dart rename to lib/account_manager/cubits/active_local_account_cubit.dart index 70392f5..bc28a92 100644 --- a/lib/account_manager/cubit/active_user_login_cubit/active_user_login_cubit.dart +++ b/lib/account_manager/cubits/active_local_account_cubit.dart @@ -3,12 +3,10 @@ import 'dart:async'; import 'package:bloc/bloc.dart'; import 'package:veilid_support/veilid_support.dart'; -import '../../repository/account_repository/account_repository.dart'; +import '../repository/account_repository/account_repository.dart'; -part 'active_user_login_state.dart'; - -class ActiveUserLoginCubit extends Cubit { - ActiveUserLoginCubit(AccountRepository accountRepository) +class ActiveLocalAccountCubit extends Cubit { + ActiveLocalAccountCubit(AccountRepository accountRepository) : _accountRepository = accountRepository, super(null) { // Subscribe to streams @@ -18,8 +16,8 @@ class ActiveUserLoginCubit extends Cubit { void _initAccountRepositorySubscription() { _accountRepositorySubscription = _accountRepository.stream.listen((change) { switch (change) { - case AccountRepositoryChange.activeUserLogin: - emit(_accountRepository.getActiveUserLogin()); + case AccountRepositoryChange.activeLocalAccount: + emit(_accountRepository.getActiveLocalAccount()); break; // Ignore these case AccountRepositoryChange.localAccounts: diff --git a/lib/account_manager/cubits/cubits.dart b/lib/account_manager/cubits/cubits.dart new file mode 100644 index 0000000..6d2875d --- /dev/null +++ b/lib/account_manager/cubits/cubits.dart @@ -0,0 +1,4 @@ +export 'account_record_cubit.dart'; +export 'active_local_account_cubit.dart'; +export 'local_accounts_cubit.dart'; +export 'user_logins_cubit.dart'; diff --git a/lib/account_manager/cubit/local_accounts_cubit/local_accounts_cubit.dart b/lib/account_manager/cubits/local_accounts_cubit.dart similarity index 73% rename from lib/account_manager/cubit/local_accounts_cubit/local_accounts_cubit.dart rename to lib/account_manager/cubits/local_accounts_cubit.dart index ee9aa81..457b8ba 100644 --- a/lib/account_manager/cubit/local_accounts_cubit/local_accounts_cubit.dart +++ b/lib/account_manager/cubits/local_accounts_cubit.dart @@ -2,17 +2,14 @@ import 'dart:async'; import 'package:bloc/bloc.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; -import 'package:veilid_support/veilid_support.dart'; -import '../../models/models.dart'; -import '../../repository/account_repository/account_repository.dart'; +import '../models/models.dart'; +import '../repository/account_repository/account_repository.dart'; -part 'local_accounts_state.dart'; - -class LocalAccountsCubit extends Cubit { +class LocalAccountsCubit extends Cubit> { LocalAccountsCubit(AccountRepository accountRepository) : _accountRepository = accountRepository, - super(LocalAccountsState()) { + super(IList()) { // Subscribe to streams _initAccountRepositorySubscription(); } @@ -25,7 +22,7 @@ class LocalAccountsCubit extends Cubit { break; // Ignore these case AccountRepositoryChange.userLogins: - case AccountRepositoryChange.activeUserLogin: + case AccountRepositoryChange.activeLocalAccount: break; } }); diff --git a/lib/account_manager/cubit/user_logins_cubit/user_logins_cubit.dart b/lib/account_manager/cubits/user_logins_cubit.dart similarity index 74% rename from lib/account_manager/cubit/user_logins_cubit/user_logins_cubit.dart rename to lib/account_manager/cubits/user_logins_cubit.dart index 9e7aee1..56dbab5 100644 --- a/lib/account_manager/cubit/user_logins_cubit/user_logins_cubit.dart +++ b/lib/account_manager/cubits/user_logins_cubit.dart @@ -2,17 +2,14 @@ import 'dart:async'; import 'package:bloc/bloc.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; -import 'package:veilid_support/veilid_support.dart'; -import '../../models/models.dart'; -import '../../repository/account_repository/account_repository.dart'; +import '../models/models.dart'; +import '../repository/account_repository/account_repository.dart'; -part 'user_logins_state.dart'; - -class UserLoginsCubit extends Cubit { +class UserLoginsCubit extends Cubit> { UserLoginsCubit(AccountRepository accountRepository) : _accountRepository = accountRepository, - super(UserLoginsState()) { + super(IList()) { // Subscribe to streams _initAccountRepositorySubscription(); } @@ -25,7 +22,7 @@ class UserLoginsCubit extends Cubit { break; // Ignore these case AccountRepositoryChange.localAccounts: - case AccountRepositoryChange.activeUserLogin: + case AccountRepositoryChange.activeLocalAccount: break; } }); diff --git a/lib/account_manager/repository/account_repository/account_repository.dart b/lib/account_manager/repository/account_repository/account_repository.dart index 60d493e..1042523 100644 --- a/lib/account_manager/repository/account_repository/account_repository.dart +++ b/lib/account_manager/repository/account_repository/account_repository.dart @@ -6,16 +6,16 @@ import 'package:veilid_support/veilid_support.dart'; import '../../../../proto/proto.dart' as proto; import '../../../tools/tools.dart'; import '../../models/models.dart'; -import 'active_logins.dart'; const String veilidChatAccountKey = 'com.veilid.veilidchat'; -enum AccountRepositoryChange { localAccounts, userLogins, activeUserLogin } +enum AccountRepositoryChange { localAccounts, userLogins, activeLocalAccount } class AccountRepository { AccountRepository._() : _localAccounts = _initLocalAccounts(), - _activeLogins = _initActiveLogins(), + _userLogins = _initUserLogins(), + _activeLocalAccount = _initActiveAccount(), _streamController = StreamController.broadcast(); @@ -28,16 +28,23 @@ class AccountRepository { : IList(), valueToJson: (val) => val.toJson((la) => la.toJson())); - static TableDBValue _initActiveLogins() => TableDBValue( + static TableDBValue> _initUserLogins() => TableDBValue( tableName: 'local_account_manager', - tableKeyName: 'active_logins', + tableKeyName: 'user_logins', valueFromJson: (obj) => obj != null - ? ActiveLogins.fromJson(obj as Map) - : ActiveLogins.empty(), - valueToJson: (val) => val.toJson()); + ? IList.fromJson(obj, genericFromJson(UserLogin.fromJson)) + : IList(), + valueToJson: (val) => val.toJson((la) => la.toJson())); + + static TableDBValue _initActiveAccount() => TableDBValue( + tableName: 'local_account_manager', + tableKeyName: 'active_local_account', + valueFromJson: (obj) => obj == null ? null : TypedKey.fromJson(obj), + valueToJson: (val) => val?.toJson()); final TableDBValue> _localAccounts; - final TableDBValue _activeLogins; + final TableDBValue> _userLogins; + final TableDBValue _activeLocalAccount; final StreamController _streamController; ////////////////////////////////////////////////////////////// @@ -47,7 +54,8 @@ class AccountRepository { Future init() async { await _localAccounts.load(); - await _activeLogins.load(); + await _userLogins.load(); + await _activeLocalAccount.load(); await _openLoggedInDHTRecords(); } @@ -59,10 +67,16 @@ class AccountRepository { ////////////////////////////////////////////////////////////// /// Selectors IList getLocalAccounts() => _localAccounts.requireValue; - IList getUserLogins() => _activeLogins.requireValue.userLogins; - TypedKey? getActiveUserLogin() => _activeLogins.requireValue.activeUserLogin; + TypedKey? getActiveLocalAccount() => _activeLocalAccount.requireValue; + IList getUserLogins() => _userLogins.requireValue; + UserLogin? getActiveUserLogin() { + final activeLocalAccount = _activeLocalAccount.requireValue; + return activeLocalAccount == null + ? null + : fetchUserLogin(activeLocalAccount); + } - LocalAccount? fetchLocalAccount({required TypedKey accountMasterRecordKey}) { + LocalAccount? fetchLocalAccount(TypedKey accountMasterRecordKey) { final localAccounts = _localAccounts.requireValue; final idx = localAccounts.indexWhere( (e) => e.identityMaster.masterRecordKey == accountMasterRecordKey); @@ -72,8 +86,8 @@ class AccountRepository { return localAccounts[idx]; } - UserLogin? fetchUserLogin({required TypedKey accountMasterRecordKey}) { - final userLogins = _activeLogins.requireValue.userLogins; + UserLogin? fetchUserLogin(TypedKey accountMasterRecordKey) { + final userLogins = _userLogins.requireValue; final idx = userLogins .indexWhere((e) => e.accountMasterRecordKey == accountMasterRecordKey); if (idx == -1) { @@ -82,34 +96,33 @@ class AccountRepository { return userLogins[idx]; } - AccountInfo? getAccountInfo({TypedKey? accountMasterRecordKey}) { - // Get active user if we have one + AccountInfo getAccountInfo(TypedKey? accountMasterRecordKey) { + // Get active account if we have one + final activeLocalAccount = getActiveLocalAccount(); if (accountMasterRecordKey == null) { - final activeUserLogin = getActiveUserLogin(); - if (activeUserLogin == null) { + if (activeLocalAccount == null) { // No user logged in - return null; + return const AccountInfo( + status: AccountInfoStatus.noAccount, + active: false, + activeAccountInfo: null); } - accountMasterRecordKey = activeUserLogin; + accountMasterRecordKey = activeLocalAccount; } + final active = accountMasterRecordKey == activeLocalAccount; // Get which local account we want to fetch the profile for - final localAccount = - fetchLocalAccount(accountMasterRecordKey: accountMasterRecordKey); + final localAccount = fetchLocalAccount(accountMasterRecordKey); if (localAccount == null) { - // Local account does not exist - return const AccountInfo( + // account does not exist + return AccountInfo( status: AccountInfoStatus.noAccount, - active: false, + active: active, activeAccountInfo: null); } // See if we've logged into this account or if it is locked - final activeUserLogin = getActiveUserLogin(); - final active = activeUserLogin == accountMasterRecordKey; - - final userLogin = - fetchUserLogin(accountMasterRecordKey: accountMasterRecordKey); + final userLogin = fetchUserLogin(accountMasterRecordKey); if (userLogin == null) { // Account was locked return AccountInfo( @@ -268,22 +281,20 @@ class AccountRepository { /// Delete an account from all devices Future switchToAccount(TypedKey? accountMasterRecordKey) async { - final activeLogins = await _activeLogins.get(); + final activeLocalAccount = await _activeLocalAccount.get(); - if (activeLogins.activeUserLogin == accountMasterRecordKey) { + if (activeLocalAccount == accountMasterRecordKey) { // Nothing to do return; } if (accountMasterRecordKey != null) { // Assert the specified record key can be found, will throw if not - final _ = activeLogins.userLogins.firstWhere( + final _ = _userLogins.requireValue.firstWhere( (ul) => ul.accountMasterRecordKey == accountMasterRecordKey); } - final newActiveLogins = - activeLogins.copyWith(activeUserLogin: accountMasterRecordKey); - await _activeLogins.set(newActiveLogins); - _streamController.add(AccountRepositoryChange.activeUserLogin); + await _activeLocalAccount.set(accountMasterRecordKey); + _streamController.add(AccountRepositoryChange.activeLocalAccount); } Future _decryptedLogin( @@ -301,25 +312,25 @@ class AccountRepository { identitySecret: identitySecret, accountKey: veilidChatAccountKey); // Add to user logins and select it - final activeLogins = await _activeLogins.get(); + final userLogins = await _userLogins.get(); final now = Veilid.instance.now(); - final newActiveLogins = activeLogins.copyWith( - userLogins: activeLogins.userLogins.replaceFirstWhere( - (ul) => ul.accountMasterRecordKey == identityMaster.masterRecordKey, - (ul) => ul != null - ? ul.copyWith(lastActive: now) - : UserLogin( - accountMasterRecordKey: identityMaster.masterRecordKey, - identitySecret: - TypedSecret(kind: cs.kind(), value: identitySecret), - accountRecordInfo: accountRecordInfo, - lastActive: now), - addIfNotFound: true), - activeUserLogin: identityMaster.masterRecordKey); - await _activeLogins.set(newActiveLogins); + final newUserLogins = userLogins.replaceFirstWhere( + (ul) => ul.accountMasterRecordKey == identityMaster.masterRecordKey, + (ul) => ul != null + ? ul.copyWith(lastActive: now) + : UserLogin( + accountMasterRecordKey: identityMaster.masterRecordKey, + identitySecret: + TypedSecret(kind: cs.kind(), value: identitySecret), + accountRecordInfo: accountRecordInfo, + lastActive: now), + addIfNotFound: true); + + await _userLogins.set(newUserLogins); + await _activeLocalAccount.set(identityMaster.masterRecordKey); _streamController - ..add(AccountRepositoryChange.activeUserLogin) - ..add(AccountRepositoryChange.userLogins); + ..add(AccountRepositoryChange.userLogins) + ..add(AccountRepositoryChange.activeLocalAccount); // Ensure all logins are opened await _openLoggedInDHTRecords(); @@ -355,34 +366,31 @@ class AccountRepository { Future logout(TypedKey? accountMasterRecordKey) async { // Resolve which user to log out - final activeLogins = await _activeLogins.get(); - final logoutUser = accountMasterRecordKey ?? activeLogins.activeUserLogin; + //final userLogins = await _userLogins.get(); + final activeLocalAccount = await _activeLocalAccount.get(); + final logoutUser = accountMasterRecordKey ?? activeLocalAccount; if (logoutUser == null) { log.error('missing user in logout: $accountMasterRecordKey'); return; } - final logoutUserLogin = fetchUserLogin(accountMasterRecordKey: logoutUser); - if (logoutUserLogin != null) { - // Close DHT records for this account - final pool = DHTRecordPool.instance; - final accountRecordKey = - logoutUserLogin.accountRecordInfo.accountRecord.recordKey; - final accountRecord = pool.getOpenedRecord(accountRecordKey); - await accountRecord?.close(); + final logoutUserLogin = fetchUserLogin(logoutUser); + if (logoutUserLogin == null) { + // Already logged out + return; } + // Close DHT records for this account + final pool = DHTRecordPool.instance; + final accountRecordKey = + logoutUserLogin.accountRecordInfo.accountRecord.recordKey; + final accountRecord = pool.getOpenedRecord(accountRecordKey); + await accountRecord?.close(); + // Remove user from active logins list - final newActiveLogins = activeLogins.copyWith( - activeUserLogin: activeLogins.activeUserLogin == logoutUser - ? null - : activeLogins.activeUserLogin, - userLogins: activeLogins.userLogins - .removeWhere((ul) => ul.accountMasterRecordKey == logoutUser)); - await _activeLogins.set(newActiveLogins); - if (activeLogins.activeUserLogin == logoutUser) { - _streamController.add(AccountRepositoryChange.activeUserLogin); - } + final newUserLogins = (await _userLogins.get()) + .removeWhere((ul) => ul.accountMasterRecordKey == logoutUser); + await _userLogins.set(newUserLogins); _streamController.add(AccountRepositoryChange.userLogins); } @@ -390,15 +398,15 @@ class AccountRepository { final pool = DHTRecordPool.instance; // For all user logins if they arent open yet - final activeLogins = await _activeLogins.get(); - for (final userLogin in activeLogins.userLogins) { + final userLogins = await _userLogins.get(); + for (final userLogin in userLogins) { //// Account record key ///////////////////////////// final accountRecordKey = userLogin.accountRecordInfo.accountRecord.recordKey; final existingAccountRecord = pool.getOpenedRecord(accountRecordKey); if (existingAccountRecord == null) { - final localAccount = fetchLocalAccount( - accountMasterRecordKey: userLogin.accountMasterRecordKey); + final localAccount = + fetchLocalAccount(userLogin.accountMasterRecordKey); // Record not yet open, do it final record = await pool.openOwned( @@ -413,8 +421,8 @@ class AccountRepository { Future _closeLoggedInDHTRecords() async { final pool = DHTRecordPool.instance; - final activeLogins = await _activeLogins.get(); - for (final userLogin in activeLogins.userLogins) { + final userLogins = await _userLogins.get(); + for (final userLogin in userLogins) { //// Account record key ///////////////////////////// final accountRecordKey = userLogin.accountRecordInfo.accountRecord.recordKey; diff --git a/lib/account_manager/repository/account_repository/active_logins.dart b/lib/account_manager/repository/account_repository/active_logins.dart deleted file mode 100644 index 9d4e713..0000000 --- a/lib/account_manager/repository/account_repository/active_logins.dart +++ /dev/null @@ -1,25 +0,0 @@ -// Represents a set of user logins and the currently selected account -import 'package:fast_immutable_collections/fast_immutable_collections.dart'; -import 'package:freezed_annotation/freezed_annotation.dart'; -import 'package:veilid_support/veilid_support.dart'; - -import '../../models/models.dart'; - -part 'active_logins.g.dart'; -part 'active_logins.freezed.dart'; - -@freezed -class ActiveLogins with _$ActiveLogins { - const factory ActiveLogins({ - // The list of current logged in accounts - required IList userLogins, - // The current selected account indexed by master record key - TypedKey? activeUserLogin, - }) = _ActiveLogins; - - factory ActiveLogins.empty() => - const ActiveLogins(userLogins: IListConst([])); - - factory ActiveLogins.fromJson(dynamic json) => - _ActiveLogins.fromJson(json as Map); -} diff --git a/lib/account_manager/repository/account_repository/active_logins.freezed.dart b/lib/account_manager/repository/account_repository/active_logins.freezed.dart deleted file mode 100644 index cf9c434..0000000 --- a/lib/account_manager/repository/account_repository/active_logins.freezed.dart +++ /dev/null @@ -1,181 +0,0 @@ -// coverage:ignore-file -// GENERATED CODE - DO NOT MODIFY BY HAND -// ignore_for_file: type=lint -// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark - -part of 'active_logins.dart'; - -// ************************************************************************** -// FreezedGenerator -// ************************************************************************** - -T _$identity(T value) => value; - -final _privateConstructorUsedError = UnsupportedError( - 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); - -ActiveLogins _$ActiveLoginsFromJson(Map json) { - return _ActiveLogins.fromJson(json); -} - -/// @nodoc -mixin _$ActiveLogins { -// The list of current logged in accounts - IList get userLogins => - throw _privateConstructorUsedError; // The current selected account indexed by master record key - Typed? get activeUserLogin => - throw _privateConstructorUsedError; - - Map toJson() => throw _privateConstructorUsedError; - @JsonKey(ignore: true) - $ActiveLoginsCopyWith get copyWith => - throw _privateConstructorUsedError; -} - -/// @nodoc -abstract class $ActiveLoginsCopyWith<$Res> { - factory $ActiveLoginsCopyWith( - ActiveLogins value, $Res Function(ActiveLogins) then) = - _$ActiveLoginsCopyWithImpl<$Res, ActiveLogins>; - @useResult - $Res call( - {IList userLogins, - Typed? activeUserLogin}); -} - -/// @nodoc -class _$ActiveLoginsCopyWithImpl<$Res, $Val extends ActiveLogins> - implements $ActiveLoginsCopyWith<$Res> { - _$ActiveLoginsCopyWithImpl(this._value, this._then); - - // ignore: unused_field - final $Val _value; - // ignore: unused_field - final $Res Function($Val) _then; - - @pragma('vm:prefer-inline') - @override - $Res call({ - Object? userLogins = null, - Object? activeUserLogin = freezed, - }) { - return _then(_value.copyWith( - userLogins: null == userLogins - ? _value.userLogins - : userLogins // ignore: cast_nullable_to_non_nullable - as IList, - activeUserLogin: freezed == activeUserLogin - ? _value.activeUserLogin - : activeUserLogin // ignore: cast_nullable_to_non_nullable - as Typed?, - ) as $Val); - } -} - -/// @nodoc -abstract class _$$ActiveLoginsImplCopyWith<$Res> - implements $ActiveLoginsCopyWith<$Res> { - factory _$$ActiveLoginsImplCopyWith( - _$ActiveLoginsImpl value, $Res Function(_$ActiveLoginsImpl) then) = - __$$ActiveLoginsImplCopyWithImpl<$Res>; - @override - @useResult - $Res call( - {IList userLogins, - Typed? activeUserLogin}); -} - -/// @nodoc -class __$$ActiveLoginsImplCopyWithImpl<$Res> - extends _$ActiveLoginsCopyWithImpl<$Res, _$ActiveLoginsImpl> - implements _$$ActiveLoginsImplCopyWith<$Res> { - __$$ActiveLoginsImplCopyWithImpl( - _$ActiveLoginsImpl _value, $Res Function(_$ActiveLoginsImpl) _then) - : super(_value, _then); - - @pragma('vm:prefer-inline') - @override - $Res call({ - Object? userLogins = null, - Object? activeUserLogin = freezed, - }) { - return _then(_$ActiveLoginsImpl( - userLogins: null == userLogins - ? _value.userLogins - : userLogins // ignore: cast_nullable_to_non_nullable - as IList, - activeUserLogin: freezed == activeUserLogin - ? _value.activeUserLogin - : activeUserLogin // ignore: cast_nullable_to_non_nullable - as Typed?, - )); - } -} - -/// @nodoc -@JsonSerializable() -class _$ActiveLoginsImpl implements _ActiveLogins { - const _$ActiveLoginsImpl({required this.userLogins, this.activeUserLogin}); - - factory _$ActiveLoginsImpl.fromJson(Map json) => - _$$ActiveLoginsImplFromJson(json); - -// The list of current logged in accounts - @override - final IList userLogins; -// The current selected account indexed by master record key - @override - final Typed? activeUserLogin; - - @override - String toString() { - return 'ActiveLogins(userLogins: $userLogins, activeUserLogin: $activeUserLogin)'; - } - - @override - bool operator ==(Object other) { - return identical(this, other) || - (other.runtimeType == runtimeType && - other is _$ActiveLoginsImpl && - const DeepCollectionEquality() - .equals(other.userLogins, userLogins) && - (identical(other.activeUserLogin, activeUserLogin) || - other.activeUserLogin == activeUserLogin)); - } - - @JsonKey(ignore: true) - @override - int get hashCode => Object.hash(runtimeType, - const DeepCollectionEquality().hash(userLogins), activeUserLogin); - - @JsonKey(ignore: true) - @override - @pragma('vm:prefer-inline') - _$$ActiveLoginsImplCopyWith<_$ActiveLoginsImpl> get copyWith => - __$$ActiveLoginsImplCopyWithImpl<_$ActiveLoginsImpl>(this, _$identity); - - @override - Map toJson() { - return _$$ActiveLoginsImplToJson( - this, - ); - } -} - -abstract class _ActiveLogins implements ActiveLogins { - const factory _ActiveLogins( - {required final IList userLogins, - final Typed? activeUserLogin}) = _$ActiveLoginsImpl; - - factory _ActiveLogins.fromJson(Map json) = - _$ActiveLoginsImpl.fromJson; - - @override // The list of current logged in accounts - IList get userLogins; - @override // The current selected account indexed by master record key - Typed? get activeUserLogin; - @override - @JsonKey(ignore: true) - _$$ActiveLoginsImplCopyWith<_$ActiveLoginsImpl> get copyWith => - throw _privateConstructorUsedError; -} diff --git a/lib/account_manager/repository/account_repository/active_logins.g.dart b/lib/account_manager/repository/account_repository/active_logins.g.dart deleted file mode 100644 index 95646a6..0000000 --- a/lib/account_manager/repository/account_repository/active_logins.g.dart +++ /dev/null @@ -1,24 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'active_logins.dart'; - -// ************************************************************************** -// JsonSerializableGenerator -// ************************************************************************** - -_$ActiveLoginsImpl _$$ActiveLoginsImplFromJson(Map json) => - _$ActiveLoginsImpl( - userLogins: IList.fromJson( - json['user_logins'], (value) => UserLogin.fromJson(value)), - activeUserLogin: json['active_user_login'] == null - ? null - : Typed.fromJson(json['active_user_login']), - ); - -Map _$$ActiveLoginsImplToJson(_$ActiveLoginsImpl instance) => - { - 'user_logins': instance.userLogins.toJson( - (value) => value.toJson(), - ), - 'active_user_login': instance.activeUserLogin?.toJson(), - }; diff --git a/lib/app.dart b/lib/app.dart index e7569c0..6624602 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -47,9 +47,9 @@ class VeilidChatApp extends StatelessWidget { create: (context) => UserLoginsCubit(AccountRepository.instance), ), - BlocProvider( + BlocProvider( create: (context) => - ActiveUserLoginCubit(AccountRepository.instance), + ActiveLocalAccountCubit(AccountRepository.instance), ), BlocProvider( create: (context) => diff --git a/lib/chat/cubits/active_chat_cubit.dart b/lib/chat/cubits/active_chat_cubit.dart index e47caec..6215098 100644 --- a/lib/chat/cubits/active_chat_cubit.dart +++ b/lib/chat/cubits/active_chat_cubit.dart @@ -2,9 +2,12 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:veilid_support/veilid_support.dart'; class ActiveChatCubit extends Cubit { - ActiveChatCubit(super.initialState); + ActiveChatCubit(super.initialState, this.setHasActiveChat); void setActiveChat(TypedKey? activeChatRemoteConversationRecordKey) { + setHasActiveChat(activeChatRemoteConversationRecordKey != null); emit(activeChatRemoteConversationRecordKey); } + + void Function(bool) setHasActiveChat; } diff --git a/lib/layout/home/home_account_ready/home_account_ready_shell.dart b/lib/layout/home/home_account_ready/home_account_ready_shell.dart index 22e1266..03fb82e 100644 --- a/lib/layout/home/home_account_ready/home_account_ready_shell.dart +++ b/lib/layout/home/home_account_ready/home_account_ready_shell.dart @@ -7,6 +7,7 @@ import '../../../chat/chat.dart'; import '../../../chat_list/chat_list.dart'; import '../../../contact_invitation/contact_invitation.dart'; import '../../../contacts/contacts.dart'; +import '../../../router/router.dart'; import '../../../tools/tools.dart'; class HomeAccountReadyShell extends StatefulWidget { @@ -18,58 +19,28 @@ class HomeAccountReadyShell extends StatefulWidget { final Widget child; } -class HomeAccountReadyShellState extends State - with TickerProviderStateMixin { +class HomeAccountReadyShellState extends State { // @override void initState() { super.initState(); } - // xxx figure out how to do this switch - - // Widget buildWithLogin(BuildContext context) { - // final activeUserLogin = context.watch().state; - - // if (activeUserLogin == null) { - // // If no logged in user is active, show the loading panel - // return const HomeNoActive(); - // } - - // final accountInfo = AccountRepository.instance - // .getAccountInfo(accountMasterRecordKey: activeUserLogin)!; - - // switch (accountInfo.status) { - // case AccountInfoStatus.noAccount: - // return const HomeAccountMissing(); - // case AccountInfoStatus.accountInvalid: - // return const HomeAccountInvalid(); - // case AccountInfoStatus.accountLocked: - // return const HomeAccountLocked(); - // case AccountInfoStatus.accountReady: - // return Provider.value( - // value: accountInfo.activeAccountInfo!, - // child: BlocProvider( - // create: (context) => AccountRecordCubit( - // record: accountInfo.activeAccountInfo!.accountRecord), - // child: const HomeAccountReady())); - // } - // } - @override Widget build(BuildContext context) { // These must be valid already before making this widget, // per the ShellRoute above it - final activeUserLogin = context.read().state!; - final accountInfo = AccountRepository.instance - .getAccountInfo(accountMasterRecordKey: activeUserLogin)!; + final activeLocalAccount = context.read().state!; + final accountInfo = + AccountRepository.instance.getAccountInfo(activeLocalAccount); final activeAccountInfo = accountInfo.activeAccountInfo!; + final routerCubit = context.read(); return Provider.value( value: activeAccountInfo, child: BlocProvider( - create: (context) => AccountRecordCubit( - record: accountInfo.activeAccountInfo!.accountRecord), + create: (context) => + AccountRecordCubit(record: activeAccountInfo.accountRecord), child: Builder(builder: (context) { final account = context.watch().state.data?.value; @@ -92,7 +63,9 @@ class HomeAccountReadyShellState extends State BlocProvider( create: (context) => ActiveConversationsCubit( activeAccountInfo: activeAccountInfo)), - BlocProvider(create: (context) => ActiveChatCubit(null)) + BlocProvider( + create: (context) => + ActiveChatCubit(null, routerCubit.setHasActiveChat)) ], child: widget.child); }))); } diff --git a/lib/layout/home/home_shell.dart b/lib/layout/home/home_shell.dart index 1124def..86969fc 100644 --- a/lib/layout/home/home_shell.dart +++ b/lib/layout/home/home_shell.dart @@ -1,6 +1,13 @@ import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:provider/provider.dart'; +import '../../account_manager/account_manager.dart'; import '../../theme/theme.dart'; +import 'home_account_invalid.dart'; +import 'home_account_locked.dart'; +import 'home_account_missing.dart'; +import 'home_no_active.dart'; class HomeShell extends StatefulWidget { const HomeShell({required this.child, super.key}); @@ -25,6 +32,34 @@ class HomeShellState extends State { super.dispose(); } + Widget buildWithLogin(BuildContext context, Widget child) { + final activeLocalAccount = context.watch().state; + + if (activeLocalAccount == null) { + // If no logged in user is active, show the loading panel + return const HomeNoActive(); + } + + final accountInfo = + AccountRepository.instance.getAccountInfo(activeLocalAccount); + + switch (accountInfo.status) { + case AccountInfoStatus.noAccount: + return const HomeAccountMissing(); + case AccountInfoStatus.accountInvalid: + return const HomeAccountInvalid(); + case AccountInfoStatus.accountLocked: + return const HomeAccountLocked(); + case AccountInfoStatus.accountReady: + return Provider.value( + value: accountInfo.activeAccountInfo!, + child: BlocProvider( + create: (context) => AccountRecordCubit( + record: accountInfo.activeAccountInfo!.accountRecord), + child: child)); + } + } + @override Widget build(BuildContext context) { final theme = Theme.of(context); @@ -37,6 +72,6 @@ class HomeShellState extends State { child: DecoratedBox( decoration: BoxDecoration( color: scale.primaryScale.activeElementBackground), - child: widget.child))); + child: buildWithLogin(context, widget.child)))); } } diff --git a/lib/router/cubit/router_cubit.dart b/lib/router/cubit/router_cubit.dart index aedae69..f43df6b 100644 --- a/lib/router/cubit/router_cubit.dart +++ b/lib/router/cubit/router_cubit.dart @@ -20,8 +20,6 @@ part 'router_state.dart'; final _rootNavKey = GlobalKey(debugLabel: 'rootNavKey'); final _homeNavKey = GlobalKey(debugLabel: 'homeNavKey'); -final _readyAccountNavKey = - GlobalKey(debugLabel: 'readyAccountNavKey'); class RouterCubit extends Cubit { RouterCubit(AccountRepository accountRepository) @@ -44,12 +42,16 @@ class RouterCubit extends Cubit { hasAnyAccount: accountRepository.getLocalAccounts().isNotEmpty)); break; case AccountRepositoryChange.userLogins: - case AccountRepositoryChange.activeUserLogin: + case AccountRepositoryChange.activeLocalAccount: break; } }); } + void setHasActiveChat(bool active) { + emit(state.copyWith(hasActiveChat: active)); + } + @override Future close() async { await _accountRepositorySubscription.cancel(); @@ -63,37 +65,20 @@ class RouterCubit extends Cubit { builder: (context, state) => const IndexPage(), ), ShellRoute( - navigatorKey: _homeNavKey, - builder: (context, state, child) => HomeShell(child: child), - routes: [ - GoRoute( - path: '/home/no_active', - builder: (context, state) => const HomeNoActive(), - ), - GoRoute( - path: '/home/account_missing', - builder: (context, state) => const HomeAccountMissing(), - ), - GoRoute( - path: '/home/account_locked', - builder: (context, state) => const HomeAccountLocked(), - ), - ShellRoute( - navigatorKey: _readyAccountNavKey, - builder: (context, state, child) => - HomeAccountReadyShell(child: child), - routes: [ - GoRoute( - path: '/home', - builder: (context, state) => const HomeAccountReadyMain(), - ), - GoRoute( - path: '/home/chat', - builder: (context, state) => const HomeAccountReadyChat(), - ), - ], - ), - ]), + navigatorKey: _homeNavKey, + builder: (context, state, child) => + HomeShell(child: HomeAccountReadyShell(child: child)), + routes: [ + GoRoute( + path: '/home', + builder: (context, state) => const HomeAccountReadyMain(), + ), + GoRoute( + path: '/home/chat', + builder: (context, state) => const HomeAccountReadyChat(), + ), + ], + ), GoRoute( path: '/new_account', builder: (context, state) => const NewAccountPage(), @@ -127,9 +112,6 @@ class RouterCubit extends Cubit { if (!state.hasAnyAccount) { return '/new_account'; } - if (!state.hasActiveChat) { xxx stop using hasActiveChat here... we need a pager for the accounts and a way to get the current account state maybe a 'activeAccountCubit' or something, we may have this alraeady but it needs to work even if logged out.`` - return '/home/no_active'; - } if (responsiveVisibility( context: context, tablet: false, @@ -144,9 +126,6 @@ class RouterCubit extends Cubit { if (!state.hasAnyAccount) { return '/new_account'; } - if (!state.hasActiveChat) { - return '/home/no_active'; - } if (responsiveVisibility( context: context, tablet: false, @@ -159,21 +138,6 @@ class RouterCubit extends Cubit { return '/home'; } return null; - case '/home/no_active': - if (state.hasActiveChat) { - return '/home'; - } - return null; - case '/home/account_missing': - if (!state.hasActiveChat) { - return '/home/no_active'; - } - return null; - case '/home/account_locked': - if (!state.hasActiveChat) { - return '/home/no_active'; - } - return null; case '/settings': return null; case '/developer': From 5cec42335155d5544e127ab7f3fec70bdd232afb Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Wed, 14 Feb 2024 21:33:15 -0500 Subject: [PATCH 38/68] more debugging --- .../account_repository.dart | 6 +- .../views/contact_invitation_display.dart | 7 +- .../views/contact_invitation_item_widget.dart | 2 - .../views/invite_dialog.dart | 13 +- .../new_contact_invitation_bottom_sheet.dart | 13 +- .../views/paste_invite_dialog.dart | 15 +- .../views/scan_invite_dialog.dart | 14 +- .../views/send_invite_dialog.dart | 22 +- .../main_pager/main_pager.dart | 36 +- lib/proto/veilidchat.pb.dart | 979 ++++++------------ lib/router/cubit/router_cubit.dart | 4 +- lib/router/cubit/router_cubit.freezed.dart | 14 +- packages/veilid_support/lib/src/config.dart | 7 + 13 files changed, 424 insertions(+), 708 deletions(-) diff --git a/lib/account_manager/repository/account_repository/account_repository.dart b/lib/account_manager/repository/account_repository/account_repository.dart index 1042523..0ce8ce7 100644 --- a/lib/account_manager/repository/account_repository/account_repository.dart +++ b/lib/account_manager/repository/account_repository/account_repository.dart @@ -53,9 +53,9 @@ class AccountRepository { static AccountRepository instance = AccountRepository._(); Future init() async { - await _localAccounts.load(); - await _userLogins.load(); - await _activeLocalAccount.load(); + await _localAccounts.get(); + await _userLogins.get(); + await _activeLocalAccount.get(); await _openLoggedInDHTRecords(); } diff --git a/lib/contact_invitation/views/contact_invitation_display.dart b/lib/contact_invitation/views/contact_invitation_display.dart index f54994e..4e75f57 100644 --- a/lib/contact_invitation/views/contact_invitation_display.dart +++ b/lib/contact_invitation/views/contact_invitation_display.dart @@ -1,4 +1,3 @@ -import 'dart:async'; import 'dart:math'; import 'package:awesome_extensions/awesome_extensions.dart'; @@ -20,12 +19,10 @@ class InvitationGeneratorCubit extends FutureCubit { class ContactInvitationDisplayDialog extends StatefulWidget { const ContactInvitationDisplayDialog({ required this.message, - required this.generator, super.key, }); final String message; - final FutureOr generator; @override ContactInvitationDisplayDialogState createState() => @@ -34,9 +31,7 @@ class ContactInvitationDisplayDialog extends StatefulWidget { @override void debugFillProperties(DiagnosticPropertiesBuilder properties) { super.debugFillProperties(properties); - properties - ..add(StringProperty('message', message)) - ..add(DiagnosticsProperty?>('generator', generator)); + properties.add(StringProperty('message', message)); } } diff --git a/lib/contact_invitation/views/contact_invitation_item_widget.dart b/lib/contact_invitation/views/contact_invitation_item_widget.dart index cb54d28..9881a93 100644 --- a/lib/contact_invitation/views/contact_invitation_item_widget.dart +++ b/lib/contact_invitation/views/contact_invitation_item_widget.dart @@ -102,8 +102,6 @@ class ContactInvitationItemWidget extends StatelessWidget { contactInvitationRecord.invitation))), child: ContactInvitationDisplayDialog( message: contactInvitationRecord.message, - generator: Uint8List.fromList( - contactInvitationRecord.invitation), ))); }, title: Text( diff --git a/lib/contact_invitation/views/invite_dialog.dart b/lib/contact_invitation/views/invite_dialog.dart index f4fe055..6e4580b 100644 --- a/lib/contact_invitation/views/invite_dialog.dart +++ b/lib/contact_invitation/views/invite_dialog.dart @@ -13,7 +13,8 @@ import '../contact_invitation.dart'; class InviteDialog extends StatefulWidget { const InviteDialog( - {required this.onValidationCancelled, + {required this.modalContext, + required this.onValidationCancelled, required this.onValidationSuccess, required this.onValidationFailed, required this.inviteControlIsValid, @@ -29,6 +30,7 @@ class InviteDialog extends StatefulWidget { InviteDialogState dialogState, Future Function({required Uint8List inviteData}) validateInviteData) buildInviteControl; + final BuildContext modalContext; @override InviteDialogState createState() => InviteDialogState(); @@ -50,7 +52,8 @@ class InviteDialog extends StatefulWidget { InviteDialogState dialogState, Future Function({required Uint8List inviteData}) validateInviteData)>.has( - 'buildInviteControl', buildInviteControl)); + 'buildInviteControl', buildInviteControl)) + ..add(DiagnosticsProperty('modalContext', modalContext)); } } @@ -69,8 +72,8 @@ class InviteDialogState extends State { Future _onAccept() async { final navigator = Navigator.of(context); - final activeAccountInfo = context.read(); - final contactList = context.read(); + final activeAccountInfo = widget.modalContext.read(); + final contactList = widget.modalContext.read(); setState(() { _isAccepting = true; @@ -133,7 +136,7 @@ class InviteDialogState extends State { }) async { try { final contactInvitationListCubit = - context.read(); + widget.modalContext.read(); setState(() { _isValidating = true; diff --git a/lib/contact_invitation/views/new_contact_invitation_bottom_sheet.dart b/lib/contact_invitation/views/new_contact_invitation_bottom_sheet.dart index 9430146..b0ba5c3 100644 --- a/lib/contact_invitation/views/new_contact_invitation_bottom_sheet.dart +++ b/lib/contact_invitation/views/new_contact_invitation_bottom_sheet.dart @@ -8,8 +8,9 @@ import 'paste_invite_dialog.dart'; import 'scan_invite_dialog.dart'; import 'send_invite_dialog.dart'; -Widget newContactInvitationBottomSheetBuilder(BuildContext context) { - final theme = Theme.of(context); +Widget newContactInvitationBottomSheetBuilder( + BuildContext sheetContext, BuildContext context) { + final theme = Theme.of(sheetContext); final textTheme = theme.textTheme; final scale = theme.extension()!; @@ -17,7 +18,7 @@ Widget newContactInvitationBottomSheetBuilder(BuildContext context) { focusNode: FocusNode(), onKeyEvent: (ke) { if (ke.logicalKey == LogicalKeyboardKey.escape) { - Navigator.pop(context); + Navigator.pop(sheetContext); } }, child: SizedBox( @@ -30,7 +31,7 @@ Widget newContactInvitationBottomSheetBuilder(BuildContext context) { Column(children: [ IconButton( onPressed: () async { - Navigator.pop(context); + Navigator.pop(sheetContext); await SendInviteDialog.show(context); }, iconSize: 64, @@ -41,7 +42,7 @@ Widget newContactInvitationBottomSheetBuilder(BuildContext context) { Column(children: [ IconButton( onPressed: () async { - Navigator.pop(context); + Navigator.pop(sheetContext); await ScanInviteDialog.show(context); }, iconSize: 64, @@ -52,7 +53,7 @@ Widget newContactInvitationBottomSheetBuilder(BuildContext context) { Column(children: [ IconButton( onPressed: () async { - Navigator.pop(context); + Navigator.pop(sheetContext); await PasteInviteDialog.show(context); }, iconSize: 64, diff --git a/lib/contact_invitation/views/paste_invite_dialog.dart b/lib/contact_invitation/views/paste_invite_dialog.dart index 545a48a..bfd3fcd 100644 --- a/lib/contact_invitation/views/paste_invite_dialog.dart +++ b/lib/contact_invitation/views/paste_invite_dialog.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'dart:typed_data'; import 'package:awesome_extensions/awesome_extensions.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_translate/flutter_translate.dart'; import 'package:veilid_support/veilid_support.dart'; @@ -11,7 +12,7 @@ import '../../tools/tools.dart'; import 'invite_dialog.dart'; class PasteInviteDialog extends StatefulWidget { - const PasteInviteDialog({super.key}); + const PasteInviteDialog({required this.modalContext, super.key}); @override PasteInviteDialogState createState() => PasteInviteDialogState(); @@ -20,7 +21,16 @@ class PasteInviteDialog extends StatefulWidget { await showStyledDialog( context: context, title: translate('paste_invite_dialog.title'), - child: const PasteInviteDialog()); + child: PasteInviteDialog(modalContext: context)); + } + + final BuildContext modalContext; + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + .add(DiagnosticsProperty('modalContext', modalContext)); } } @@ -122,6 +132,7 @@ class PasteInviteDialogState extends State { // ignore: prefer_expression_function_bodies Widget build(BuildContext context) { return InviteDialog( + modalContext: widget.modalContext, onValidationCancelled: onValidationCancelled, onValidationSuccess: onValidationSuccess, onValidationFailed: onValidationFailed, diff --git a/lib/contact_invitation/views/scan_invite_dialog.dart b/lib/contact_invitation/views/scan_invite_dialog.dart index 67c0999..70f5b3b 100644 --- a/lib/contact_invitation/views/scan_invite_dialog.dart +++ b/lib/contact_invitation/views/scan_invite_dialog.dart @@ -104,7 +104,7 @@ class ScannerOverlay extends CustomPainter { } class ScanInviteDialog extends StatefulWidget { - const ScanInviteDialog({super.key}); + const ScanInviteDialog({required this.modalContext, super.key}); @override ScanInviteDialogState createState() => ScanInviteDialogState(); @@ -113,7 +113,16 @@ class ScanInviteDialog extends StatefulWidget { await showStyledDialog( context: context, title: translate('scan_invite_dialog.title'), - child: const ScanInviteDialog()); + child: ScanInviteDialog(modalContext: context)); + } + + final BuildContext modalContext; + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + .add(DiagnosticsProperty('modalContext', modalContext)); } } @@ -380,6 +389,7 @@ class ScanInviteDialogState extends State { // ignore: prefer_expression_function_bodies Widget build(BuildContext context) { return InviteDialog( + modalContext: widget.modalContext, onValidationCancelled: onValidationCancelled, onValidationSuccess: onValidationSuccess, onValidationFailed: onValidationFailed, diff --git a/lib/contact_invitation/views/send_invite_dialog.dart b/lib/contact_invitation/views/send_invite_dialog.dart index 399ffe8..8113078 100644 --- a/lib/contact_invitation/views/send_invite_dialog.dart +++ b/lib/contact_invitation/views/send_invite_dialog.dart @@ -14,7 +14,7 @@ import '../../tools/tools.dart'; import '../contact_invitation.dart'; class SendInviteDialog extends StatefulWidget { - const SendInviteDialog({super.key}); + const SendInviteDialog({required this.modalContext, super.key}); @override SendInviteDialogState createState() => SendInviteDialogState(); @@ -23,7 +23,16 @@ class SendInviteDialog extends StatefulWidget { await showStyledDialog( context: context, title: translate('send_invite_dialog.title'), - child: const SendInviteDialog()); + child: SendInviteDialog(modalContext: context)); + } + + final BuildContext modalContext; + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + .add(DiagnosticsProperty('modalContext', modalContext)); } } @@ -132,7 +141,7 @@ class SendInviteDialogState extends State { // Start generation final contactInvitationListCubit = - context.read(); + widget.modalContext.read(); final generator = contactInvitationListCubit.createInvitation( encryptionKeyType: _encryptionKeyType, @@ -145,10 +154,11 @@ class SendInviteDialogState extends State { } await showDialog( context: context, - builder: (context) => ContactInvitationDisplayDialog( + builder: (context) => BlocProvider( + create: (context) => InvitationGeneratorCubit(generator), + child: ContactInvitationDisplayDialog( message: _messageTextController.text, - generator: generator, - )); + ))); // if (ret == null) { // return; // } diff --git a/lib/layout/home/home_account_ready/main_pager/main_pager.dart b/lib/layout/home/home_account_ready/main_pager/main_pager.dart index 2cbee9c..54f794c 100644 --- a/lib/layout/home/home_account_ready/main_pager/main_pager.dart +++ b/lib/layout/home/home_account_ready/main_pager/main_pager.dart @@ -110,37 +110,38 @@ class MainPagerState extends State with TickerProviderStateMixin { context: context, // ignore: prefer_expression_function_bodies builder: (context) { - return const AlertDialog( - shape: RoundedRectangleBorder( + return AlertDialog( + shape: const RoundedRectangleBorder( borderRadius: BorderRadius.all(Radius.circular(20)), ), - contentPadding: EdgeInsets.only( + contentPadding: const EdgeInsets.only( top: 10, ), - title: Text( + title: const Text( 'Scan Contact Invite', style: TextStyle(fontSize: 24), ), - content: ScanInviteDialog()); + content: ScanInviteDialog( + modalContext: context, + )); }); } - // ignore: prefer_expression_function_bodies - Widget _onNewChatBottomSheetBuilder(BuildContext context) { - return const SizedBox( - height: 200, - child: Center( - child: Text( - 'Group and custom chat functionality is not available yet'))); - } + Widget _onNewChatBottomSheetBuilder( + BuildContext sheetContext, BuildContext context) => + const SizedBox( + height: 200, + child: Center( + child: Text( + 'Group and custom chat functionality is not available yet'))); - Widget _bottomSheetBuilder(BuildContext context) { + Widget _bottomSheetBuilder(BuildContext sheetContext, BuildContext context) { if (_currentPage == 0) { // New contact invitation - return newContactInvitationBottomSheetBuilder(context); + return newContactInvitationBottomSheetBuilder(sheetContext, context); } else if (_currentPage == 1) { // New chat - return _onNewChatBottomSheetBuilder(context); + return _onNewChatBottomSheetBuilder(sheetContext, context); } else { // Unknown error return debugPage('unknown page'); @@ -214,7 +215,8 @@ class MainPagerState extends State with TickerProviderStateMixin { _fabIconList[_currentPage], color: scale.secondaryScale.text, ), - bottomSheetBuilder: _bottomSheetBuilder), + bottomSheetBuilder: (sheetContext) => + _bottomSheetBuilder(sheetContext, context)), floatingActionButtonLocation: FloatingActionButtonLocation.endDocked, ); } diff --git a/lib/proto/veilidchat.pb.dart b/lib/proto/veilidchat.pb.dart index af40e07..a0db194 100644 --- a/lib/proto/veilidchat.pb.dart +++ b/lib/proto/veilidchat.pb.dart @@ -23,38 +23,28 @@ export 'veilidchat.pbenum.dart'; class Attachment extends $pb.GeneratedMessage { factory Attachment() => create(); Attachment._() : super(); - factory Attachment.fromBuffer($core.List<$core.int> i, - [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => - create()..mergeFromBuffer(i, r); - factory Attachment.fromJson($core.String i, - [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => - create()..mergeFromJson(i, r); + factory Attachment.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory Attachment.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); - static final $pb.BuilderInfo _i = $pb.BuilderInfo( - _omitMessageNames ? '' : 'Attachment', - package: const $pb.PackageName(_omitMessageNames ? '' : 'veilidchat'), - createEmptyInstance: create) - ..e(1, _omitFieldNames ? '' : 'kind', $pb.PbFieldType.OE, - defaultOrMaker: AttachmentKind.ATTACHMENT_KIND_UNSPECIFIED, - valueOf: AttachmentKind.valueOf, - enumValues: AttachmentKind.values) + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'Attachment', package: const $pb.PackageName(_omitMessageNames ? '' : 'veilidchat'), createEmptyInstance: create) + ..e(1, _omitFieldNames ? '' : 'kind', $pb.PbFieldType.OE, defaultOrMaker: AttachmentKind.ATTACHMENT_KIND_UNSPECIFIED, valueOf: AttachmentKind.valueOf, enumValues: AttachmentKind.values) ..aOS(2, _omitFieldNames ? '' : 'mime') ..aOS(3, _omitFieldNames ? '' : 'name') - ..aOM<$0.DataReference>(4, _omitFieldNames ? '' : 'content', - subBuilder: $0.DataReference.create) - ..aOM<$1.Signature>(5, _omitFieldNames ? '' : 'signature', - subBuilder: $1.Signature.create) - ..hasRequiredFields = false; + ..aOM<$0.DataReference>(4, _omitFieldNames ? '' : 'content', subBuilder: $0.DataReference.create) + ..aOM<$1.Signature>(5, _omitFieldNames ? '' : 'signature', subBuilder: $1.Signature.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') + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') Attachment clone() => Attachment()..mergeFromMessage(this); - @$core.Deprecated('Using this can add significant overhead to your binary. ' - 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' - 'Will be removed in next major version') - Attachment copyWith(void Function(Attachment) updates) => - super.copyWith((message) => updates(message as Attachment)) as Attachment; + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + Attachment copyWith(void Function(Attachment) updates) => super.copyWith((message) => updates(message as Attachment)) as Attachment; $pb.BuilderInfo get info_ => _i; @@ -63,17 +53,13 @@ class Attachment extends $pb.GeneratedMessage { Attachment createEmptyInstance() => create(); static $pb.PbList createRepeated() => $pb.PbList(); @$core.pragma('dart2js:noInline') - static Attachment getDefault() => _defaultInstance ??= - $pb.GeneratedMessage.$_defaultFor(create); + static Attachment getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); static Attachment? _defaultInstance; @$pb.TagNumber(1) AttachmentKind get kind => $_getN(0); @$pb.TagNumber(1) - set kind(AttachmentKind v) { - setField(1, v); - } - + set kind(AttachmentKind v) { setField(1, v); } @$pb.TagNumber(1) $core.bool hasKind() => $_has(0); @$pb.TagNumber(1) @@ -82,10 +68,7 @@ class Attachment extends $pb.GeneratedMessage { @$pb.TagNumber(2) $core.String get mime => $_getSZ(1); @$pb.TagNumber(2) - set mime($core.String v) { - $_setString(1, v); - } - + set mime($core.String v) { $_setString(1, v); } @$pb.TagNumber(2) $core.bool hasMime() => $_has(1); @$pb.TagNumber(2) @@ -94,10 +77,7 @@ class Attachment extends $pb.GeneratedMessage { @$pb.TagNumber(3) $core.String get name => $_getSZ(2); @$pb.TagNumber(3) - set name($core.String v) { - $_setString(2, v); - } - + set name($core.String v) { $_setString(2, v); } @$pb.TagNumber(3) $core.bool hasName() => $_has(2); @$pb.TagNumber(3) @@ -106,10 +86,7 @@ class Attachment extends $pb.GeneratedMessage { @$pb.TagNumber(4) $0.DataReference get content => $_getN(3); @$pb.TagNumber(4) - set content($0.DataReference v) { - setField(4, v); - } - + set content($0.DataReference v) { setField(4, v); } @$pb.TagNumber(4) $core.bool hasContent() => $_has(3); @$pb.TagNumber(4) @@ -120,10 +97,7 @@ class Attachment extends $pb.GeneratedMessage { @$pb.TagNumber(5) $1.Signature get signature => $_getN(4); @$pb.TagNumber(5) - set signature($1.Signature v) { - setField(5, v); - } - + set signature($1.Signature v) { setField(5, v); } @$pb.TagNumber(5) $core.bool hasSignature() => $_has(4); @$pb.TagNumber(5) @@ -135,39 +109,28 @@ class Attachment extends $pb.GeneratedMessage { class Message extends $pb.GeneratedMessage { factory Message() => create(); Message._() : super(); - factory Message.fromBuffer($core.List<$core.int> i, - [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => - create()..mergeFromBuffer(i, r); - factory Message.fromJson($core.String i, - [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => - create()..mergeFromJson(i, r); + factory Message.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory Message.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); - static final $pb.BuilderInfo _i = $pb.BuilderInfo( - _omitMessageNames ? '' : 'Message', - package: const $pb.PackageName(_omitMessageNames ? '' : 'veilidchat'), - createEmptyInstance: create) - ..aOM<$1.TypedKey>(1, _omitFieldNames ? '' : 'author', - subBuilder: $1.TypedKey.create) - ..a<$fixnum.Int64>( - 2, _omitFieldNames ? '' : 'timestamp', $pb.PbFieldType.OU6, - defaultOrMaker: $fixnum.Int64.ZERO) + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'Message', package: const $pb.PackageName(_omitMessageNames ? '' : 'veilidchat'), createEmptyInstance: create) + ..aOM<$1.TypedKey>(1, _omitFieldNames ? '' : 'author', subBuilder: $1.TypedKey.create) + ..a<$fixnum.Int64>(2, _omitFieldNames ? '' : 'timestamp', $pb.PbFieldType.OU6, defaultOrMaker: $fixnum.Int64.ZERO) ..aOS(3, _omitFieldNames ? '' : 'text') - ..aOM<$1.Signature>(4, _omitFieldNames ? '' : 'signature', - subBuilder: $1.Signature.create) - ..pc( - 5, _omitFieldNames ? '' : 'attachments', $pb.PbFieldType.PM, - subBuilder: Attachment.create) - ..hasRequiredFields = false; + ..aOM<$1.Signature>(4, _omitFieldNames ? '' : 'signature', subBuilder: $1.Signature.create) + ..pc(5, _omitFieldNames ? '' : 'attachments', $pb.PbFieldType.PM, subBuilder: Attachment.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') + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') Message clone() => Message()..mergeFromMessage(this); - @$core.Deprecated('Using this can add significant overhead to your binary. ' - 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' - 'Will be removed in next major version') - Message copyWith(void Function(Message) updates) => - super.copyWith((message) => updates(message as Message)) as Message; + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + Message copyWith(void Function(Message) updates) => super.copyWith((message) => updates(message as Message)) as Message; $pb.BuilderInfo get info_ => _i; @@ -176,17 +139,13 @@ class Message extends $pb.GeneratedMessage { Message createEmptyInstance() => create(); static $pb.PbList createRepeated() => $pb.PbList(); @$core.pragma('dart2js:noInline') - static Message getDefault() => - _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static Message getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); static Message? _defaultInstance; @$pb.TagNumber(1) $1.TypedKey get author => $_getN(0); @$pb.TagNumber(1) - set author($1.TypedKey v) { - setField(1, v); - } - + set author($1.TypedKey v) { setField(1, v); } @$pb.TagNumber(1) $core.bool hasAuthor() => $_has(0); @$pb.TagNumber(1) @@ -197,10 +156,7 @@ class Message extends $pb.GeneratedMessage { @$pb.TagNumber(2) $fixnum.Int64 get timestamp => $_getI64(1); @$pb.TagNumber(2) - set timestamp($fixnum.Int64 v) { - $_setInt64(1, v); - } - + set timestamp($fixnum.Int64 v) { $_setInt64(1, v); } @$pb.TagNumber(2) $core.bool hasTimestamp() => $_has(1); @$pb.TagNumber(2) @@ -209,10 +165,7 @@ class Message extends $pb.GeneratedMessage { @$pb.TagNumber(3) $core.String get text => $_getSZ(2); @$pb.TagNumber(3) - set text($core.String v) { - $_setString(2, v); - } - + set text($core.String v) { $_setString(2, v); } @$pb.TagNumber(3) $core.bool hasText() => $_has(2); @$pb.TagNumber(3) @@ -221,10 +174,7 @@ class Message extends $pb.GeneratedMessage { @$pb.TagNumber(4) $1.Signature get signature => $_getN(3); @$pb.TagNumber(4) - set signature($1.Signature v) { - setField(4, v); - } - + set signature($1.Signature v) { setField(4, v); } @$pb.TagNumber(4) $core.bool hasSignature() => $_has(3); @$pb.TagNumber(4) @@ -239,54 +189,41 @@ class Message extends $pb.GeneratedMessage { class Conversation extends $pb.GeneratedMessage { factory Conversation() => create(); Conversation._() : super(); - factory Conversation.fromBuffer($core.List<$core.int> i, - [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => - create()..mergeFromBuffer(i, r); - factory Conversation.fromJson($core.String i, - [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => - create()..mergeFromJson(i, r); + factory Conversation.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory Conversation.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); - static final $pb.BuilderInfo _i = $pb.BuilderInfo( - _omitMessageNames ? '' : 'Conversation', - package: const $pb.PackageName(_omitMessageNames ? '' : 'veilidchat'), - createEmptyInstance: create) - ..aOM(1, _omitFieldNames ? '' : 'profile', - subBuilder: Profile.create) + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'Conversation', package: const $pb.PackageName(_omitMessageNames ? '' : 'veilidchat'), createEmptyInstance: create) + ..aOM(1, _omitFieldNames ? '' : 'profile', subBuilder: Profile.create) ..aOS(2, _omitFieldNames ? '' : 'identityMasterJson') - ..aOM<$1.TypedKey>(3, _omitFieldNames ? '' : 'messages', - subBuilder: $1.TypedKey.create) - ..hasRequiredFields = false; + ..aOM<$1.TypedKey>(3, _omitFieldNames ? '' : 'messages', subBuilder: $1.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') + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') Conversation clone() => Conversation()..mergeFromMessage(this); - @$core.Deprecated('Using this can add significant overhead to your binary. ' - 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' - 'Will be removed in next major version') - Conversation copyWith(void Function(Conversation) updates) => - super.copyWith((message) => updates(message as Conversation)) - as Conversation; + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + Conversation copyWith(void Function(Conversation) updates) => super.copyWith((message) => updates(message as Conversation)) as Conversation; $pb.BuilderInfo get info_ => _i; @$core.pragma('dart2js:noInline') static Conversation create() => Conversation._(); Conversation createEmptyInstance() => create(); - static $pb.PbList createRepeated() => - $pb.PbList(); + static $pb.PbList createRepeated() => $pb.PbList(); @$core.pragma('dart2js:noInline') - static Conversation getDefault() => _defaultInstance ??= - $pb.GeneratedMessage.$_defaultFor(create); + static Conversation getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); static Conversation? _defaultInstance; @$pb.TagNumber(1) Profile get profile => $_getN(0); @$pb.TagNumber(1) - set profile(Profile v) { - setField(1, v); - } - + set profile(Profile v) { setField(1, v); } @$pb.TagNumber(1) $core.bool hasProfile() => $_has(0); @$pb.TagNumber(1) @@ -297,10 +234,7 @@ class Conversation extends $pb.GeneratedMessage { @$pb.TagNumber(2) $core.String get identityMasterJson => $_getSZ(1); @$pb.TagNumber(2) - set identityMasterJson($core.String v) { - $_setString(1, v); - } - + set identityMasterJson($core.String v) { $_setString(1, v); } @$pb.TagNumber(2) $core.bool hasIdentityMasterJson() => $_has(1); @$pb.TagNumber(2) @@ -309,10 +243,7 @@ class Conversation extends $pb.GeneratedMessage { @$pb.TagNumber(3) $1.TypedKey get messages => $_getN(2); @$pb.TagNumber(3) - set messages($1.TypedKey v) { - setField(3, v); - } - + set messages($1.TypedKey v) { setField(3, v); } @$pb.TagNumber(3) $core.bool hasMessages() => $_has(2); @$pb.TagNumber(3) @@ -324,40 +255,30 @@ class Conversation extends $pb.GeneratedMessage { class Contact extends $pb.GeneratedMessage { factory Contact() => create(); Contact._() : super(); - factory Contact.fromBuffer($core.List<$core.int> i, - [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => - create()..mergeFromBuffer(i, r); - factory Contact.fromJson($core.String i, - [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => - create()..mergeFromJson(i, r); + factory Contact.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory Contact.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); - static final $pb.BuilderInfo _i = $pb.BuilderInfo( - _omitMessageNames ? '' : 'Contact', - package: const $pb.PackageName(_omitMessageNames ? '' : 'veilidchat'), - createEmptyInstance: create) - ..aOM(1, _omitFieldNames ? '' : 'editedProfile', - subBuilder: Profile.create) - ..aOM(2, _omitFieldNames ? '' : 'remoteProfile', - subBuilder: Profile.create) + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'Contact', package: const $pb.PackageName(_omitMessageNames ? '' : 'veilidchat'), createEmptyInstance: create) + ..aOM(1, _omitFieldNames ? '' : 'editedProfile', subBuilder: Profile.create) + ..aOM(2, _omitFieldNames ? '' : 'remoteProfile', subBuilder: Profile.create) ..aOS(3, _omitFieldNames ? '' : 'identityMasterJson') - ..aOM<$1.TypedKey>(4, _omitFieldNames ? '' : 'identityPublicKey', - subBuilder: $1.TypedKey.create) - ..aOM<$1.TypedKey>(5, _omitFieldNames ? '' : 'remoteConversationRecordKey', - subBuilder: $1.TypedKey.create) - ..aOM<$1.TypedKey>(6, _omitFieldNames ? '' : 'localConversationRecordKey', - subBuilder: $1.TypedKey.create) + ..aOM<$1.TypedKey>(4, _omitFieldNames ? '' : 'identityPublicKey', subBuilder: $1.TypedKey.create) + ..aOM<$1.TypedKey>(5, _omitFieldNames ? '' : 'remoteConversationRecordKey', subBuilder: $1.TypedKey.create) + ..aOM<$1.TypedKey>(6, _omitFieldNames ? '' : 'localConversationRecordKey', subBuilder: $1.TypedKey.create) ..aOB(7, _omitFieldNames ? '' : 'showAvailability') - ..hasRequiredFields = false; + ..hasRequiredFields = false + ; - @$core.Deprecated('Using this can add significant overhead to your binary. ' - 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' - 'Will be removed in next major version') + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') Contact clone() => Contact()..mergeFromMessage(this); - @$core.Deprecated('Using this can add significant overhead to your binary. ' - 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' - 'Will be removed in next major version') - Contact copyWith(void Function(Contact) updates) => - super.copyWith((message) => updates(message as Contact)) as Contact; + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + Contact copyWith(void Function(Contact) updates) => super.copyWith((message) => updates(message as Contact)) as Contact; $pb.BuilderInfo get info_ => _i; @@ -366,17 +287,13 @@ class Contact extends $pb.GeneratedMessage { Contact createEmptyInstance() => create(); static $pb.PbList createRepeated() => $pb.PbList(); @$core.pragma('dart2js:noInline') - static Contact getDefault() => - _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static Contact getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); static Contact? _defaultInstance; @$pb.TagNumber(1) Profile get editedProfile => $_getN(0); @$pb.TagNumber(1) - set editedProfile(Profile v) { - setField(1, v); - } - + set editedProfile(Profile v) { setField(1, v); } @$pb.TagNumber(1) $core.bool hasEditedProfile() => $_has(0); @$pb.TagNumber(1) @@ -387,10 +304,7 @@ class Contact extends $pb.GeneratedMessage { @$pb.TagNumber(2) Profile get remoteProfile => $_getN(1); @$pb.TagNumber(2) - set remoteProfile(Profile v) { - setField(2, v); - } - + set remoteProfile(Profile v) { setField(2, v); } @$pb.TagNumber(2) $core.bool hasRemoteProfile() => $_has(1); @$pb.TagNumber(2) @@ -401,10 +315,7 @@ class Contact extends $pb.GeneratedMessage { @$pb.TagNumber(3) $core.String get identityMasterJson => $_getSZ(2); @$pb.TagNumber(3) - set identityMasterJson($core.String v) { - $_setString(2, v); - } - + set identityMasterJson($core.String v) { $_setString(2, v); } @$pb.TagNumber(3) $core.bool hasIdentityMasterJson() => $_has(2); @$pb.TagNumber(3) @@ -413,10 +324,7 @@ class Contact extends $pb.GeneratedMessage { @$pb.TagNumber(4) $1.TypedKey get identityPublicKey => $_getN(3); @$pb.TagNumber(4) - set identityPublicKey($1.TypedKey v) { - setField(4, v); - } - + set identityPublicKey($1.TypedKey v) { setField(4, v); } @$pb.TagNumber(4) $core.bool hasIdentityPublicKey() => $_has(3); @$pb.TagNumber(4) @@ -427,10 +335,7 @@ class Contact extends $pb.GeneratedMessage { @$pb.TagNumber(5) $1.TypedKey get remoteConversationRecordKey => $_getN(4); @$pb.TagNumber(5) - set remoteConversationRecordKey($1.TypedKey v) { - setField(5, v); - } - + set remoteConversationRecordKey($1.TypedKey v) { setField(5, v); } @$pb.TagNumber(5) $core.bool hasRemoteConversationRecordKey() => $_has(4); @$pb.TagNumber(5) @@ -441,10 +346,7 @@ class Contact extends $pb.GeneratedMessage { @$pb.TagNumber(6) $1.TypedKey get localConversationRecordKey => $_getN(5); @$pb.TagNumber(6) - set localConversationRecordKey($1.TypedKey v) { - setField(6, v); - } - + set localConversationRecordKey($1.TypedKey v) { setField(6, v); } @$pb.TagNumber(6) $core.bool hasLocalConversationRecordKey() => $_has(5); @$pb.TagNumber(6) @@ -455,10 +357,7 @@ class Contact extends $pb.GeneratedMessage { @$pb.TagNumber(7) $core.bool get showAvailability => $_getBF(6); @$pb.TagNumber(7) - set showAvailability($core.bool v) { - $_setBool(6, v); - } - + set showAvailability($core.bool v) { $_setBool(6, v); } @$pb.TagNumber(7) $core.bool hasShowAvailability() => $_has(6); @$pb.TagNumber(7) @@ -468,38 +367,28 @@ class Contact extends $pb.GeneratedMessage { class Profile extends $pb.GeneratedMessage { factory Profile() => create(); Profile._() : super(); - factory Profile.fromBuffer($core.List<$core.int> i, - [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => - create()..mergeFromBuffer(i, r); - factory Profile.fromJson($core.String i, - [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => - create()..mergeFromJson(i, r); + factory Profile.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory Profile.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); - static final $pb.BuilderInfo _i = $pb.BuilderInfo( - _omitMessageNames ? '' : 'Profile', - package: const $pb.PackageName(_omitMessageNames ? '' : 'veilidchat'), - createEmptyInstance: create) + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'Profile', package: const $pb.PackageName(_omitMessageNames ? '' : 'veilidchat'), createEmptyInstance: create) ..aOS(1, _omitFieldNames ? '' : 'name') ..aOS(2, _omitFieldNames ? '' : 'pronouns') ..aOS(3, _omitFieldNames ? '' : 'status') - ..e( - 4, _omitFieldNames ? '' : 'availability', $pb.PbFieldType.OE, - defaultOrMaker: Availability.AVAILABILITY_UNSPECIFIED, - valueOf: Availability.valueOf, - enumValues: Availability.values) - ..aOM<$1.TypedKey>(5, _omitFieldNames ? '' : 'avatar', - subBuilder: $1.TypedKey.create) - ..hasRequiredFields = false; + ..e(4, _omitFieldNames ? '' : 'availability', $pb.PbFieldType.OE, defaultOrMaker: Availability.AVAILABILITY_UNSPECIFIED, valueOf: Availability.valueOf, enumValues: Availability.values) + ..aOM<$1.TypedKey>(5, _omitFieldNames ? '' : 'avatar', subBuilder: $1.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') + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') Profile clone() => Profile()..mergeFromMessage(this); - @$core.Deprecated('Using this can add significant overhead to your binary. ' - 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' - 'Will be removed in next major version') - Profile copyWith(void Function(Profile) updates) => - super.copyWith((message) => updates(message as Profile)) as Profile; + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + Profile copyWith(void Function(Profile) updates) => super.copyWith((message) => updates(message as Profile)) as Profile; $pb.BuilderInfo get info_ => _i; @@ -508,17 +397,13 @@ class Profile extends $pb.GeneratedMessage { Profile createEmptyInstance() => create(); static $pb.PbList createRepeated() => $pb.PbList(); @$core.pragma('dart2js:noInline') - static Profile getDefault() => - _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static Profile getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); static Profile? _defaultInstance; @$pb.TagNumber(1) $core.String get name => $_getSZ(0); @$pb.TagNumber(1) - set name($core.String v) { - $_setString(0, v); - } - + set name($core.String v) { $_setString(0, v); } @$pb.TagNumber(1) $core.bool hasName() => $_has(0); @$pb.TagNumber(1) @@ -527,10 +412,7 @@ class Profile extends $pb.GeneratedMessage { @$pb.TagNumber(2) $core.String get pronouns => $_getSZ(1); @$pb.TagNumber(2) - set pronouns($core.String v) { - $_setString(1, v); - } - + set pronouns($core.String v) { $_setString(1, v); } @$pb.TagNumber(2) $core.bool hasPronouns() => $_has(1); @$pb.TagNumber(2) @@ -539,10 +421,7 @@ class Profile extends $pb.GeneratedMessage { @$pb.TagNumber(3) $core.String get status => $_getSZ(2); @$pb.TagNumber(3) - set status($core.String v) { - $_setString(2, v); - } - + set status($core.String v) { $_setString(2, v); } @$pb.TagNumber(3) $core.bool hasStatus() => $_has(2); @$pb.TagNumber(3) @@ -551,10 +430,7 @@ class Profile extends $pb.GeneratedMessage { @$pb.TagNumber(4) Availability get availability => $_getN(3); @$pb.TagNumber(4) - set availability(Availability v) { - setField(4, v); - } - + set availability(Availability v) { setField(4, v); } @$pb.TagNumber(4) $core.bool hasAvailability() => $_has(3); @$pb.TagNumber(4) @@ -563,10 +439,7 @@ class Profile extends $pb.GeneratedMessage { @$pb.TagNumber(5) $1.TypedKey get avatar => $_getN(4); @$pb.TagNumber(5) - set avatar($1.TypedKey v) { - setField(5, v); - } - + set avatar($1.TypedKey v) { setField(5, v); } @$pb.TagNumber(5) $core.bool hasAvatar() => $_has(4); @$pb.TagNumber(5) @@ -578,34 +451,25 @@ class Profile extends $pb.GeneratedMessage { 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); + 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', - package: const $pb.PackageName(_omitMessageNames ? '' : 'veilidchat'), - createEmptyInstance: create) - ..e(1, _omitFieldNames ? '' : 'type', $pb.PbFieldType.OE, - defaultOrMaker: ChatType.CHAT_TYPE_UNSPECIFIED, - valueOf: ChatType.valueOf, - enumValues: ChatType.values) - ..aOM<$1.TypedKey>(2, _omitFieldNames ? '' : 'remoteConversationKey', - subBuilder: $1.TypedKey.create) - ..hasRequiredFields = false; + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'Chat', package: const $pb.PackageName(_omitMessageNames ? '' : 'veilidchat'), createEmptyInstance: create) + ..e(1, _omitFieldNames ? '' : 'type', $pb.PbFieldType.OE, defaultOrMaker: ChatType.CHAT_TYPE_UNSPECIFIED, valueOf: ChatType.valueOf, enumValues: ChatType.values) + ..aOM<$1.TypedKey>(2, _omitFieldNames ? '' : 'remoteConversationKey', subBuilder: $1.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') + @$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; + @$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; @@ -614,17 +478,13 @@ class Chat extends $pb.GeneratedMessage { Chat createEmptyInstance() => create(); static $pb.PbList createRepeated() => $pb.PbList(); @$core.pragma('dart2js:noInline') - static Chat getDefault() => - _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + 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); - } - + set type(ChatType v) { setField(1, v); } @$pb.TagNumber(1) $core.bool hasType() => $_has(0); @$pb.TagNumber(1) @@ -633,10 +493,7 @@ class Chat extends $pb.GeneratedMessage { @$pb.TagNumber(2) $1.TypedKey get remoteConversationKey => $_getN(1); @$pb.TagNumber(2) - set remoteConversationKey($1.TypedKey v) { - setField(2, v); - } - + set remoteConversationKey($1.TypedKey v) { setField(2, v); } @$pb.TagNumber(2) $core.bool hasRemoteConversationKey() => $_has(1); @$pb.TagNumber(2) @@ -648,40 +505,29 @@ class Chat extends $pb.GeneratedMessage { class Account extends $pb.GeneratedMessage { factory Account() => create(); Account._() : super(); - factory Account.fromBuffer($core.List<$core.int> i, - [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => - create()..mergeFromBuffer(i, r); - factory Account.fromJson($core.String i, - [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => - create()..mergeFromJson(i, r); + factory Account.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory Account.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); - static final $pb.BuilderInfo _i = $pb.BuilderInfo( - _omitMessageNames ? '' : 'Account', - package: const $pb.PackageName(_omitMessageNames ? '' : 'veilidchat'), - createEmptyInstance: create) - ..aOM(1, _omitFieldNames ? '' : 'profile', - subBuilder: Profile.create) + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'Account', package: const $pb.PackageName(_omitMessageNames ? '' : 'veilidchat'), createEmptyInstance: create) + ..aOM(1, _omitFieldNames ? '' : 'profile', subBuilder: Profile.create) ..aOB(2, _omitFieldNames ? '' : 'invisible') - ..a<$core.int>( - 3, _omitFieldNames ? '' : 'autoAwayTimeoutSec', $pb.PbFieldType.OU3) - ..aOM<$0.OwnedDHTRecordPointer>(4, _omitFieldNames ? '' : 'contactList', - subBuilder: $0.OwnedDHTRecordPointer.create) - ..aOM<$0.OwnedDHTRecordPointer>( - 5, _omitFieldNames ? '' : 'contactInvitationRecords', - subBuilder: $0.OwnedDHTRecordPointer.create) - ..aOM<$0.OwnedDHTRecordPointer>(6, _omitFieldNames ? '' : 'chatList', - subBuilder: $0.OwnedDHTRecordPointer.create) - ..hasRequiredFields = false; + ..a<$core.int>(3, _omitFieldNames ? '' : 'autoAwayTimeoutSec', $pb.PbFieldType.OU3) + ..aOM<$0.OwnedDHTRecordPointer>(4, _omitFieldNames ? '' : 'contactList', subBuilder: $0.OwnedDHTRecordPointer.create) + ..aOM<$0.OwnedDHTRecordPointer>(5, _omitFieldNames ? '' : 'contactInvitationRecords', subBuilder: $0.OwnedDHTRecordPointer.create) + ..aOM<$0.OwnedDHTRecordPointer>(6, _omitFieldNames ? '' : 'chatList', subBuilder: $0.OwnedDHTRecordPointer.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') + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') Account clone() => Account()..mergeFromMessage(this); - @$core.Deprecated('Using this can add significant overhead to your binary. ' - 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' - 'Will be removed in next major version') - Account copyWith(void Function(Account) updates) => - super.copyWith((message) => updates(message as Account)) as Account; + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + Account copyWith(void Function(Account) updates) => super.copyWith((message) => updates(message as Account)) as Account; $pb.BuilderInfo get info_ => _i; @@ -690,17 +536,13 @@ class Account extends $pb.GeneratedMessage { Account createEmptyInstance() => create(); static $pb.PbList createRepeated() => $pb.PbList(); @$core.pragma('dart2js:noInline') - static Account getDefault() => - _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static Account getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); static Account? _defaultInstance; @$pb.TagNumber(1) Profile get profile => $_getN(0); @$pb.TagNumber(1) - set profile(Profile v) { - setField(1, v); - } - + set profile(Profile v) { setField(1, v); } @$pb.TagNumber(1) $core.bool hasProfile() => $_has(0); @$pb.TagNumber(1) @@ -711,10 +553,7 @@ class Account extends $pb.GeneratedMessage { @$pb.TagNumber(2) $core.bool get invisible => $_getBF(1); @$pb.TagNumber(2) - set invisible($core.bool v) { - $_setBool(1, v); - } - + set invisible($core.bool v) { $_setBool(1, v); } @$pb.TagNumber(2) $core.bool hasInvisible() => $_has(1); @$pb.TagNumber(2) @@ -723,10 +562,7 @@ class Account extends $pb.GeneratedMessage { @$pb.TagNumber(3) $core.int get autoAwayTimeoutSec => $_getIZ(2); @$pb.TagNumber(3) - set autoAwayTimeoutSec($core.int v) { - $_setUnsignedInt32(2, v); - } - + set autoAwayTimeoutSec($core.int v) { $_setUnsignedInt32(2, v); } @$pb.TagNumber(3) $core.bool hasAutoAwayTimeoutSec() => $_has(2); @$pb.TagNumber(3) @@ -735,10 +571,7 @@ class Account extends $pb.GeneratedMessage { @$pb.TagNumber(4) $0.OwnedDHTRecordPointer get contactList => $_getN(3); @$pb.TagNumber(4) - set contactList($0.OwnedDHTRecordPointer v) { - setField(4, v); - } - + set contactList($0.OwnedDHTRecordPointer v) { setField(4, v); } @$pb.TagNumber(4) $core.bool hasContactList() => $_has(3); @$pb.TagNumber(4) @@ -749,10 +582,7 @@ class Account extends $pb.GeneratedMessage { @$pb.TagNumber(5) $0.OwnedDHTRecordPointer get contactInvitationRecords => $_getN(4); @$pb.TagNumber(5) - set contactInvitationRecords($0.OwnedDHTRecordPointer v) { - setField(5, v); - } - + set contactInvitationRecords($0.OwnedDHTRecordPointer v) { setField(5, v); } @$pb.TagNumber(5) $core.bool hasContactInvitationRecords() => $_has(4); @$pb.TagNumber(5) @@ -763,10 +593,7 @@ class Account extends $pb.GeneratedMessage { @$pb.TagNumber(6) $0.OwnedDHTRecordPointer get chatList => $_getN(5); @$pb.TagNumber(6) - set chatList($0.OwnedDHTRecordPointer v) { - setField(6, v); - } - + set chatList($0.OwnedDHTRecordPointer v) { setField(6, v); } @$pb.TagNumber(6) $core.bool hasChatList() => $_has(5); @$pb.TagNumber(6) @@ -778,53 +605,40 @@ class Account extends $pb.GeneratedMessage { class ContactInvitation extends $pb.GeneratedMessage { factory ContactInvitation() => create(); ContactInvitation._() : super(); - factory ContactInvitation.fromBuffer($core.List<$core.int> i, - [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => - create()..mergeFromBuffer(i, r); - factory ContactInvitation.fromJson($core.String i, - [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => - create()..mergeFromJson(i, r); + factory ContactInvitation.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory ContactInvitation.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); - static final $pb.BuilderInfo _i = $pb.BuilderInfo( - _omitMessageNames ? '' : 'ContactInvitation', - package: const $pb.PackageName(_omitMessageNames ? '' : 'veilidchat'), - createEmptyInstance: create) - ..aOM<$1.TypedKey>(1, _omitFieldNames ? '' : 'contactRequestInboxKey', - subBuilder: $1.TypedKey.create) - ..a<$core.List<$core.int>>( - 2, _omitFieldNames ? '' : 'writerSecret', $pb.PbFieldType.OY) - ..hasRequiredFields = false; + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'ContactInvitation', package: const $pb.PackageName(_omitMessageNames ? '' : 'veilidchat'), createEmptyInstance: create) + ..aOM<$1.TypedKey>(1, _omitFieldNames ? '' : 'contactRequestInboxKey', subBuilder: $1.TypedKey.create) + ..a<$core.List<$core.int>>(2, _omitFieldNames ? '' : 'writerSecret', $pb.PbFieldType.OY) + ..hasRequiredFields = false + ; - @$core.Deprecated('Using this can add significant overhead to your binary. ' - 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' - 'Will be removed in next major version') + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') ContactInvitation clone() => ContactInvitation()..mergeFromMessage(this); - @$core.Deprecated('Using this can add significant overhead to your binary. ' - 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' - 'Will be removed in next major version') - ContactInvitation copyWith(void Function(ContactInvitation) updates) => - super.copyWith((message) => updates(message as ContactInvitation)) - as ContactInvitation; + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + ContactInvitation copyWith(void Function(ContactInvitation) updates) => super.copyWith((message) => updates(message as ContactInvitation)) as ContactInvitation; $pb.BuilderInfo get info_ => _i; @$core.pragma('dart2js:noInline') static ContactInvitation create() => ContactInvitation._(); ContactInvitation createEmptyInstance() => create(); - static $pb.PbList createRepeated() => - $pb.PbList(); + static $pb.PbList createRepeated() => $pb.PbList(); @$core.pragma('dart2js:noInline') - static ContactInvitation getDefault() => _defaultInstance ??= - $pb.GeneratedMessage.$_defaultFor(create); + static ContactInvitation getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); static ContactInvitation? _defaultInstance; @$pb.TagNumber(1) $1.TypedKey get contactRequestInboxKey => $_getN(0); @$pb.TagNumber(1) - set contactRequestInboxKey($1.TypedKey v) { - setField(1, v); - } - + set contactRequestInboxKey($1.TypedKey v) { setField(1, v); } @$pb.TagNumber(1) $core.bool hasContactRequestInboxKey() => $_has(0); @$pb.TagNumber(1) @@ -835,10 +649,7 @@ class ContactInvitation extends $pb.GeneratedMessage { @$pb.TagNumber(2) $core.List<$core.int> get writerSecret => $_getN(1); @$pb.TagNumber(2) - set writerSecret($core.List<$core.int> v) { - $_setBytes(1, v); - } - + set writerSecret($core.List<$core.int> v) { $_setBytes(1, v); } @$pb.TagNumber(2) $core.bool hasWriterSecret() => $_has(1); @$pb.TagNumber(2) @@ -848,55 +659,40 @@ class ContactInvitation extends $pb.GeneratedMessage { class SignedContactInvitation extends $pb.GeneratedMessage { factory SignedContactInvitation() => create(); SignedContactInvitation._() : super(); - factory SignedContactInvitation.fromBuffer($core.List<$core.int> i, - [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => - create()..mergeFromBuffer(i, r); - factory SignedContactInvitation.fromJson($core.String i, - [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => - create()..mergeFromJson(i, r); + factory SignedContactInvitation.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory SignedContactInvitation.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); - static final $pb.BuilderInfo _i = $pb.BuilderInfo( - _omitMessageNames ? '' : 'SignedContactInvitation', - package: const $pb.PackageName(_omitMessageNames ? '' : 'veilidchat'), - createEmptyInstance: create) - ..a<$core.List<$core.int>>( - 1, _omitFieldNames ? '' : 'contactInvitation', $pb.PbFieldType.OY) - ..aOM<$1.Signature>(2, _omitFieldNames ? '' : 'identitySignature', - subBuilder: $1.Signature.create) - ..hasRequiredFields = false; + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'SignedContactInvitation', package: const $pb.PackageName(_omitMessageNames ? '' : 'veilidchat'), createEmptyInstance: create) + ..a<$core.List<$core.int>>(1, _omitFieldNames ? '' : 'contactInvitation', $pb.PbFieldType.OY) + ..aOM<$1.Signature>(2, _omitFieldNames ? '' : 'identitySignature', subBuilder: $1.Signature.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') - SignedContactInvitation clone() => - SignedContactInvitation()..mergeFromMessage(this); - @$core.Deprecated('Using this can add significant overhead to your binary. ' - 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' - 'Will be removed in next major version') - SignedContactInvitation copyWith( - void Function(SignedContactInvitation) updates) => - super.copyWith((message) => updates(message as SignedContactInvitation)) - as SignedContactInvitation; + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + SignedContactInvitation clone() => SignedContactInvitation()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + SignedContactInvitation copyWith(void Function(SignedContactInvitation) updates) => super.copyWith((message) => updates(message as SignedContactInvitation)) as SignedContactInvitation; $pb.BuilderInfo get info_ => _i; @$core.pragma('dart2js:noInline') static SignedContactInvitation create() => SignedContactInvitation._(); SignedContactInvitation createEmptyInstance() => create(); - static $pb.PbList createRepeated() => - $pb.PbList(); + static $pb.PbList createRepeated() => $pb.PbList(); @$core.pragma('dart2js:noInline') - static SignedContactInvitation getDefault() => _defaultInstance ??= - $pb.GeneratedMessage.$_defaultFor(create); + static SignedContactInvitation getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); static SignedContactInvitation? _defaultInstance; @$pb.TagNumber(1) $core.List<$core.int> get contactInvitation => $_getN(0); @$pb.TagNumber(1) - set contactInvitation($core.List<$core.int> v) { - $_setBytes(0, v); - } - + set contactInvitation($core.List<$core.int> v) { $_setBytes(0, v); } @$pb.TagNumber(1) $core.bool hasContactInvitation() => $_has(0); @$pb.TagNumber(1) @@ -905,10 +701,7 @@ class SignedContactInvitation extends $pb.GeneratedMessage { @$pb.TagNumber(2) $1.Signature get identitySignature => $_getN(1); @$pb.TagNumber(2) - set identitySignature($1.Signature v) { - setField(2, v); - } - + set identitySignature($1.Signature v) { setField(2, v); } @$pb.TagNumber(2) $core.bool hasIdentitySignature() => $_has(1); @$pb.TagNumber(2) @@ -920,56 +713,40 @@ class SignedContactInvitation extends $pb.GeneratedMessage { class ContactRequest extends $pb.GeneratedMessage { factory ContactRequest() => create(); ContactRequest._() : super(); - factory ContactRequest.fromBuffer($core.List<$core.int> i, - [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => - create()..mergeFromBuffer(i, r); - factory ContactRequest.fromJson($core.String i, - [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => - create()..mergeFromJson(i, r); + factory ContactRequest.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + 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', - package: const $pb.PackageName(_omitMessageNames ? '' : 'veilidchat'), - createEmptyInstance: create) - ..e( - 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; + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'ContactRequest', package: const $pb.PackageName(_omitMessageNames ? '' : 'veilidchat'), createEmptyInstance: create) + ..e(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 + ; - @$core.Deprecated('Using this can add significant overhead to your binary. ' - 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' - 'Will be removed in next major version') + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') ContactRequest clone() => ContactRequest()..mergeFromMessage(this); - @$core.Deprecated('Using this can add significant overhead to your binary. ' - 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' - 'Will be removed in next major version') - ContactRequest copyWith(void Function(ContactRequest) updates) => - super.copyWith((message) => updates(message as ContactRequest)) - as ContactRequest; + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + ContactRequest copyWith(void Function(ContactRequest) updates) => super.copyWith((message) => updates(message as ContactRequest)) as ContactRequest; $pb.BuilderInfo get info_ => _i; @$core.pragma('dart2js:noInline') static ContactRequest create() => ContactRequest._(); ContactRequest createEmptyInstance() => create(); - static $pb.PbList createRepeated() => - $pb.PbList(); + static $pb.PbList createRepeated() => $pb.PbList(); @$core.pragma('dart2js:noInline') - static ContactRequest getDefault() => _defaultInstance ??= - $pb.GeneratedMessage.$_defaultFor(create); + static ContactRequest getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); static ContactRequest? _defaultInstance; @$pb.TagNumber(1) EncryptionKeyType get encryptionKeyType => $_getN(0); @$pb.TagNumber(1) - set encryptionKeyType(EncryptionKeyType v) { - setField(1, v); - } - + set encryptionKeyType(EncryptionKeyType v) { setField(1, v); } @$pb.TagNumber(1) $core.bool hasEncryptionKeyType() => $_has(0); @$pb.TagNumber(1) @@ -978,10 +755,7 @@ class ContactRequest extends $pb.GeneratedMessage { @$pb.TagNumber(2) $core.List<$core.int> get private => $_getN(1); @$pb.TagNumber(2) - set private($core.List<$core.int> v) { - $_setBytes(1, v); - } - + set private($core.List<$core.int> v) { $_setBytes(1, v); } @$pb.TagNumber(2) $core.bool hasPrivate() => $_has(1); @$pb.TagNumber(2) @@ -991,62 +765,43 @@ class ContactRequest extends $pb.GeneratedMessage { class ContactRequestPrivate extends $pb.GeneratedMessage { factory ContactRequestPrivate() => create(); ContactRequestPrivate._() : super(); - factory ContactRequestPrivate.fromBuffer($core.List<$core.int> i, - [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => - create()..mergeFromBuffer(i, r); - factory ContactRequestPrivate.fromJson($core.String i, - [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => - create()..mergeFromJson(i, r); + factory ContactRequestPrivate.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory ContactRequestPrivate.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); - static final $pb.BuilderInfo _i = $pb.BuilderInfo( - _omitMessageNames ? '' : 'ContactRequestPrivate', - package: const $pb.PackageName(_omitMessageNames ? '' : 'veilidchat'), - createEmptyInstance: create) - ..aOM<$1.CryptoKey>(1, _omitFieldNames ? '' : 'writerKey', - subBuilder: $1.CryptoKey.create) - ..aOM(2, _omitFieldNames ? '' : 'profile', - subBuilder: Profile.create) - ..aOM<$1.TypedKey>(3, _omitFieldNames ? '' : 'identityMasterRecordKey', - subBuilder: $1.TypedKey.create) - ..aOM<$1.TypedKey>(4, _omitFieldNames ? '' : 'chatRecordKey', - subBuilder: $1.TypedKey.create) - ..a<$fixnum.Int64>( - 5, _omitFieldNames ? '' : 'expiration', $pb.PbFieldType.OU6, - defaultOrMaker: $fixnum.Int64.ZERO) - ..hasRequiredFields = false; + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'ContactRequestPrivate', package: const $pb.PackageName(_omitMessageNames ? '' : 'veilidchat'), createEmptyInstance: create) + ..aOM<$1.CryptoKey>(1, _omitFieldNames ? '' : 'writerKey', subBuilder: $1.CryptoKey.create) + ..aOM(2, _omitFieldNames ? '' : 'profile', subBuilder: Profile.create) + ..aOM<$1.TypedKey>(3, _omitFieldNames ? '' : 'identityMasterRecordKey', subBuilder: $1.TypedKey.create) + ..aOM<$1.TypedKey>(4, _omitFieldNames ? '' : 'chatRecordKey', subBuilder: $1.TypedKey.create) + ..a<$fixnum.Int64>(5, _omitFieldNames ? '' : 'expiration', $pb.PbFieldType.OU6, defaultOrMaker: $fixnum.Int64.ZERO) + ..hasRequiredFields = false + ; - @$core.Deprecated('Using this can add significant overhead to your binary. ' - 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' - 'Will be removed in next major version') - ContactRequestPrivate clone() => - ContactRequestPrivate()..mergeFromMessage(this); - @$core.Deprecated('Using this can add significant overhead to your binary. ' - 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' - 'Will be removed in next major version') - ContactRequestPrivate copyWith( - void Function(ContactRequestPrivate) updates) => - super.copyWith((message) => updates(message as ContactRequestPrivate)) - as ContactRequestPrivate; + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + ContactRequestPrivate clone() => ContactRequestPrivate()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + ContactRequestPrivate copyWith(void Function(ContactRequestPrivate) updates) => super.copyWith((message) => updates(message as ContactRequestPrivate)) as ContactRequestPrivate; $pb.BuilderInfo get info_ => _i; @$core.pragma('dart2js:noInline') static ContactRequestPrivate create() => ContactRequestPrivate._(); ContactRequestPrivate createEmptyInstance() => create(); - static $pb.PbList createRepeated() => - $pb.PbList(); + static $pb.PbList createRepeated() => $pb.PbList(); @$core.pragma('dart2js:noInline') - static ContactRequestPrivate getDefault() => _defaultInstance ??= - $pb.GeneratedMessage.$_defaultFor(create); + static ContactRequestPrivate getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); static ContactRequestPrivate? _defaultInstance; @$pb.TagNumber(1) $1.CryptoKey get writerKey => $_getN(0); @$pb.TagNumber(1) - set writerKey($1.CryptoKey v) { - setField(1, v); - } - + set writerKey($1.CryptoKey v) { setField(1, v); } @$pb.TagNumber(1) $core.bool hasWriterKey() => $_has(0); @$pb.TagNumber(1) @@ -1057,10 +812,7 @@ class ContactRequestPrivate extends $pb.GeneratedMessage { @$pb.TagNumber(2) Profile get profile => $_getN(1); @$pb.TagNumber(2) - set profile(Profile v) { - setField(2, v); - } - + set profile(Profile v) { setField(2, v); } @$pb.TagNumber(2) $core.bool hasProfile() => $_has(1); @$pb.TagNumber(2) @@ -1071,10 +823,7 @@ class ContactRequestPrivate extends $pb.GeneratedMessage { @$pb.TagNumber(3) $1.TypedKey get identityMasterRecordKey => $_getN(2); @$pb.TagNumber(3) - set identityMasterRecordKey($1.TypedKey v) { - setField(3, v); - } - + set identityMasterRecordKey($1.TypedKey v) { setField(3, v); } @$pb.TagNumber(3) $core.bool hasIdentityMasterRecordKey() => $_has(2); @$pb.TagNumber(3) @@ -1085,10 +834,7 @@ class ContactRequestPrivate extends $pb.GeneratedMessage { @$pb.TagNumber(4) $1.TypedKey get chatRecordKey => $_getN(3); @$pb.TagNumber(4) - set chatRecordKey($1.TypedKey v) { - setField(4, v); - } - + set chatRecordKey($1.TypedKey v) { setField(4, v); } @$pb.TagNumber(4) $core.bool hasChatRecordKey() => $_has(3); @$pb.TagNumber(4) @@ -1099,10 +845,7 @@ class ContactRequestPrivate extends $pb.GeneratedMessage { @$pb.TagNumber(5) $fixnum.Int64 get expiration => $_getI64(4); @$pb.TagNumber(5) - set expiration($fixnum.Int64 v) { - $_setInt64(4, v); - } - + set expiration($fixnum.Int64 v) { $_setInt64(4, v); } @$pb.TagNumber(5) $core.bool hasExpiration() => $_has(4); @$pb.TagNumber(5) @@ -1112,54 +855,41 @@ class ContactRequestPrivate extends $pb.GeneratedMessage { class ContactResponse extends $pb.GeneratedMessage { factory ContactResponse() => create(); ContactResponse._() : super(); - factory ContactResponse.fromBuffer($core.List<$core.int> i, - [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => - create()..mergeFromBuffer(i, r); - factory ContactResponse.fromJson($core.String i, - [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => - create()..mergeFromJson(i, r); + factory ContactResponse.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory ContactResponse.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); - static final $pb.BuilderInfo _i = $pb.BuilderInfo( - _omitMessageNames ? '' : 'ContactResponse', - package: const $pb.PackageName(_omitMessageNames ? '' : 'veilidchat'), - createEmptyInstance: create) + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'ContactResponse', package: const $pb.PackageName(_omitMessageNames ? '' : 'veilidchat'), createEmptyInstance: create) ..aOB(1, _omitFieldNames ? '' : 'accept') - ..aOM<$1.TypedKey>(2, _omitFieldNames ? '' : 'identityMasterRecordKey', - subBuilder: $1.TypedKey.create) - ..aOM<$1.TypedKey>(3, _omitFieldNames ? '' : 'remoteConversationRecordKey', - subBuilder: $1.TypedKey.create) - ..hasRequiredFields = false; + ..aOM<$1.TypedKey>(2, _omitFieldNames ? '' : 'identityMasterRecordKey', subBuilder: $1.TypedKey.create) + ..aOM<$1.TypedKey>(3, _omitFieldNames ? '' : 'remoteConversationRecordKey', subBuilder: $1.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') + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') ContactResponse clone() => ContactResponse()..mergeFromMessage(this); - @$core.Deprecated('Using this can add significant overhead to your binary. ' - 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' - 'Will be removed in next major version') - ContactResponse copyWith(void Function(ContactResponse) updates) => - super.copyWith((message) => updates(message as ContactResponse)) - as ContactResponse; + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + ContactResponse copyWith(void Function(ContactResponse) updates) => super.copyWith((message) => updates(message as ContactResponse)) as ContactResponse; $pb.BuilderInfo get info_ => _i; @$core.pragma('dart2js:noInline') static ContactResponse create() => ContactResponse._(); ContactResponse createEmptyInstance() => create(); - static $pb.PbList createRepeated() => - $pb.PbList(); + static $pb.PbList createRepeated() => $pb.PbList(); @$core.pragma('dart2js:noInline') - static ContactResponse getDefault() => _defaultInstance ??= - $pb.GeneratedMessage.$_defaultFor(create); + static ContactResponse getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); static ContactResponse? _defaultInstance; @$pb.TagNumber(1) $core.bool get accept => $_getBF(0); @$pb.TagNumber(1) - set accept($core.bool v) { - $_setBool(0, v); - } - + set accept($core.bool v) { $_setBool(0, v); } @$pb.TagNumber(1) $core.bool hasAccept() => $_has(0); @$pb.TagNumber(1) @@ -1168,10 +898,7 @@ class ContactResponse extends $pb.GeneratedMessage { @$pb.TagNumber(2) $1.TypedKey get identityMasterRecordKey => $_getN(1); @$pb.TagNumber(2) - set identityMasterRecordKey($1.TypedKey v) { - setField(2, v); - } - + set identityMasterRecordKey($1.TypedKey v) { setField(2, v); } @$pb.TagNumber(2) $core.bool hasIdentityMasterRecordKey() => $_has(1); @$pb.TagNumber(2) @@ -1182,10 +909,7 @@ class ContactResponse extends $pb.GeneratedMessage { @$pb.TagNumber(3) $1.TypedKey get remoteConversationRecordKey => $_getN(2); @$pb.TagNumber(3) - set remoteConversationRecordKey($1.TypedKey v) { - setField(3, v); - } - + set remoteConversationRecordKey($1.TypedKey v) { setField(3, v); } @$pb.TagNumber(3) $core.bool hasRemoteConversationRecordKey() => $_has(2); @$pb.TagNumber(3) @@ -1197,55 +921,40 @@ class ContactResponse extends $pb.GeneratedMessage { class SignedContactResponse extends $pb.GeneratedMessage { factory SignedContactResponse() => create(); SignedContactResponse._() : super(); - factory SignedContactResponse.fromBuffer($core.List<$core.int> i, - [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => - create()..mergeFromBuffer(i, r); - factory SignedContactResponse.fromJson($core.String i, - [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => - create()..mergeFromJson(i, r); + factory SignedContactResponse.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory SignedContactResponse.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); - static final $pb.BuilderInfo _i = $pb.BuilderInfo( - _omitMessageNames ? '' : 'SignedContactResponse', - package: const $pb.PackageName(_omitMessageNames ? '' : 'veilidchat'), - createEmptyInstance: create) - ..a<$core.List<$core.int>>( - 1, _omitFieldNames ? '' : 'contactResponse', $pb.PbFieldType.OY) - ..aOM<$1.Signature>(2, _omitFieldNames ? '' : 'identitySignature', - subBuilder: $1.Signature.create) - ..hasRequiredFields = false; + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'SignedContactResponse', package: const $pb.PackageName(_omitMessageNames ? '' : 'veilidchat'), createEmptyInstance: create) + ..a<$core.List<$core.int>>(1, _omitFieldNames ? '' : 'contactResponse', $pb.PbFieldType.OY) + ..aOM<$1.Signature>(2, _omitFieldNames ? '' : 'identitySignature', subBuilder: $1.Signature.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') - SignedContactResponse clone() => - SignedContactResponse()..mergeFromMessage(this); - @$core.Deprecated('Using this can add significant overhead to your binary. ' - 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' - 'Will be removed in next major version') - SignedContactResponse copyWith( - void Function(SignedContactResponse) updates) => - super.copyWith((message) => updates(message as SignedContactResponse)) - as SignedContactResponse; + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + SignedContactResponse clone() => SignedContactResponse()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + SignedContactResponse copyWith(void Function(SignedContactResponse) updates) => super.copyWith((message) => updates(message as SignedContactResponse)) as SignedContactResponse; $pb.BuilderInfo get info_ => _i; @$core.pragma('dart2js:noInline') static SignedContactResponse create() => SignedContactResponse._(); SignedContactResponse createEmptyInstance() => create(); - static $pb.PbList createRepeated() => - $pb.PbList(); + static $pb.PbList createRepeated() => $pb.PbList(); @$core.pragma('dart2js:noInline') - static SignedContactResponse getDefault() => _defaultInstance ??= - $pb.GeneratedMessage.$_defaultFor(create); + static SignedContactResponse getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); static SignedContactResponse? _defaultInstance; @$pb.TagNumber(1) $core.List<$core.int> get contactResponse => $_getN(0); @$pb.TagNumber(1) - set contactResponse($core.List<$core.int> v) { - $_setBytes(0, v); - } - + set contactResponse($core.List<$core.int> v) { $_setBytes(0, v); } @$pb.TagNumber(1) $core.bool hasContactResponse() => $_has(0); @$pb.TagNumber(1) @@ -1254,10 +963,7 @@ class SignedContactResponse extends $pb.GeneratedMessage { @$pb.TagNumber(2) $1.Signature get identitySignature => $_getN(1); @$pb.TagNumber(2) - set identitySignature($1.Signature v) { - setField(2, v); - } - + set identitySignature($1.Signature v) { setField(2, v); } @$pb.TagNumber(2) $core.bool hasIdentitySignature() => $_has(1); @$pb.TagNumber(2) @@ -1269,66 +975,45 @@ class SignedContactResponse extends $pb.GeneratedMessage { 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); + 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 ? '' : 'ContactInvitationRecord', - package: const $pb.PackageName(_omitMessageNames ? '' : 'veilidchat'), - createEmptyInstance: create) - ..aOM<$0.OwnedDHTRecordPointer>( - 1, _omitFieldNames ? '' : 'contactRequestInbox', - subBuilder: $0.OwnedDHTRecordPointer.create) - ..aOM<$1.CryptoKey>(2, _omitFieldNames ? '' : 'writerKey', - subBuilder: $1.CryptoKey.create) - ..aOM<$1.CryptoKey>(3, _omitFieldNames ? '' : 'writerSecret', - subBuilder: $1.CryptoKey.create) - ..aOM<$1.TypedKey>(4, _omitFieldNames ? '' : 'localConversationRecordKey', - subBuilder: $1.TypedKey.create) - ..a<$fixnum.Int64>( - 5, _omitFieldNames ? '' : 'expiration', $pb.PbFieldType.OU6, - defaultOrMaker: $fixnum.Int64.ZERO) - ..a<$core.List<$core.int>>( - 6, _omitFieldNames ? '' : 'invitation', $pb.PbFieldType.OY) + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'ContactInvitationRecord', package: const $pb.PackageName(_omitMessageNames ? '' : 'veilidchat'), createEmptyInstance: create) + ..aOM<$0.OwnedDHTRecordPointer>(1, _omitFieldNames ? '' : 'contactRequestInbox', subBuilder: $0.OwnedDHTRecordPointer.create) + ..aOM<$1.CryptoKey>(2, _omitFieldNames ? '' : 'writerKey', subBuilder: $1.CryptoKey.create) + ..aOM<$1.CryptoKey>(3, _omitFieldNames ? '' : 'writerSecret', subBuilder: $1.CryptoKey.create) + ..aOM<$1.TypedKey>(4, _omitFieldNames ? '' : 'localConversationRecordKey', subBuilder: $1.TypedKey.create) + ..a<$fixnum.Int64>(5, _omitFieldNames ? '' : 'expiration', $pb.PbFieldType.OU6, defaultOrMaker: $fixnum.Int64.ZERO) + ..a<$core.List<$core.int>>(6, _omitFieldNames ? '' : 'invitation', $pb.PbFieldType.OY) ..aOS(7, _omitFieldNames ? '' : 'message') - ..hasRequiredFields = false; + ..hasRequiredFields = false + ; - @$core.Deprecated('Using this can add significant overhead to your binary. ' - 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' - 'Will be removed in next major version') - 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') - ContactInvitationRecord copyWith( - void Function(ContactInvitationRecord) updates) => - super.copyWith((message) => updates(message as ContactInvitationRecord)) - as ContactInvitationRecord; + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + 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') + 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 ContactInvitationRecord create() => ContactInvitationRecord._(); ContactInvitationRecord createEmptyInstance() => create(); - static $pb.PbList createRepeated() => - $pb.PbList(); + static $pb.PbList createRepeated() => $pb.PbList(); @$core.pragma('dart2js:noInline') - static ContactInvitationRecord getDefault() => _defaultInstance ??= - $pb.GeneratedMessage.$_defaultFor(create); + static ContactInvitationRecord getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); static ContactInvitationRecord? _defaultInstance; @$pb.TagNumber(1) $0.OwnedDHTRecordPointer get contactRequestInbox => $_getN(0); @$pb.TagNumber(1) - set contactRequestInbox($0.OwnedDHTRecordPointer v) { - setField(1, v); - } - + set contactRequestInbox($0.OwnedDHTRecordPointer v) { setField(1, v); } @$pb.TagNumber(1) $core.bool hasContactRequestInbox() => $_has(0); @$pb.TagNumber(1) @@ -1339,10 +1024,7 @@ class ContactInvitationRecord extends $pb.GeneratedMessage { @$pb.TagNumber(2) $1.CryptoKey get writerKey => $_getN(1); @$pb.TagNumber(2) - set writerKey($1.CryptoKey v) { - setField(2, v); - } - + set writerKey($1.CryptoKey v) { setField(2, v); } @$pb.TagNumber(2) $core.bool hasWriterKey() => $_has(1); @$pb.TagNumber(2) @@ -1353,10 +1035,7 @@ class ContactInvitationRecord extends $pb.GeneratedMessage { @$pb.TagNumber(3) $1.CryptoKey get writerSecret => $_getN(2); @$pb.TagNumber(3) - set writerSecret($1.CryptoKey v) { - setField(3, v); - } - + set writerSecret($1.CryptoKey v) { setField(3, v); } @$pb.TagNumber(3) $core.bool hasWriterSecret() => $_has(2); @$pb.TagNumber(3) @@ -1367,10 +1046,7 @@ class ContactInvitationRecord extends $pb.GeneratedMessage { @$pb.TagNumber(4) $1.TypedKey get localConversationRecordKey => $_getN(3); @$pb.TagNumber(4) - set localConversationRecordKey($1.TypedKey v) { - setField(4, v); - } - + set localConversationRecordKey($1.TypedKey v) { setField(4, v); } @$pb.TagNumber(4) $core.bool hasLocalConversationRecordKey() => $_has(3); @$pb.TagNumber(4) @@ -1381,10 +1057,7 @@ class ContactInvitationRecord extends $pb.GeneratedMessage { @$pb.TagNumber(5) $fixnum.Int64 get expiration => $_getI64(4); @$pb.TagNumber(5) - set expiration($fixnum.Int64 v) { - $_setInt64(4, v); - } - + set expiration($fixnum.Int64 v) { $_setInt64(4, v); } @$pb.TagNumber(5) $core.bool hasExpiration() => $_has(4); @$pb.TagNumber(5) @@ -1393,10 +1066,7 @@ class ContactInvitationRecord extends $pb.GeneratedMessage { @$pb.TagNumber(6) $core.List<$core.int> get invitation => $_getN(5); @$pb.TagNumber(6) - set invitation($core.List<$core.int> v) { - $_setBytes(5, v); - } - + set invitation($core.List<$core.int> v) { $_setBytes(5, v); } @$pb.TagNumber(6) $core.bool hasInvitation() => $_has(5); @$pb.TagNumber(6) @@ -1405,16 +1075,13 @@ class ContactInvitationRecord extends $pb.GeneratedMessage { @$pb.TagNumber(7) $core.String get message => $_getSZ(6); @$pb.TagNumber(7) - set message($core.String v) { - $_setString(6, v); - } - + set message($core.String v) { $_setString(6, v); } @$pb.TagNumber(7) $core.bool hasMessage() => $_has(6); @$pb.TagNumber(7) void clearMessage() => clearField(7); } + const _omitFieldNames = $core.bool.fromEnvironment('protobuf.omit_field_names'); -const _omitMessageNames = - $core.bool.fromEnvironment('protobuf.omit_message_names'); +const _omitMessageNames = $core.bool.fromEnvironment('protobuf.omit_message_names'); diff --git a/lib/router/cubit/router_cubit.dart b/lib/router/cubit/router_cubit.dart index f43df6b..88d7537 100644 --- a/lib/router/cubit/router_cubit.dart +++ b/lib/router/cubit/router_cubit.dart @@ -31,7 +31,9 @@ class RouterCubit extends Cubit { // Watch for changes that the router will care about Future.delayed(Duration.zero, () async { await eventualInitialized.future; - emit(state.copyWith(isInitialized: true)); + emit(state.copyWith( + isInitialized: true, + hasAnyAccount: accountRepository.getLocalAccounts().isNotEmpty)); }); // Subscribe to repository streams diff --git a/lib/router/cubit/router_cubit.freezed.dart b/lib/router/cubit/router_cubit.freezed.dart index 16baed7..bbe7a04 100644 --- a/lib/router/cubit/router_cubit.freezed.dart +++ b/lib/router/cubit/router_cubit.freezed.dart @@ -118,7 +118,7 @@ class __$$RouterStateImplCopyWithImpl<$Res> /// @nodoc @JsonSerializable() -class _$RouterStateImpl implements _RouterState { +class _$RouterStateImpl with DiagnosticableTreeMixin implements _RouterState { const _$RouterStateImpl( {required this.isInitialized, required this.hasAnyAccount, @@ -135,10 +135,20 @@ class _$RouterStateImpl implements _RouterState { final bool hasActiveChat; @override - String toString() { + String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) { return 'RouterState(isInitialized: $isInitialized, hasAnyAccount: $hasAnyAccount, hasActiveChat: $hasActiveChat)'; } + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(DiagnosticsProperty('type', 'RouterState')) + ..add(DiagnosticsProperty('isInitialized', isInitialized)) + ..add(DiagnosticsProperty('hasAnyAccount', hasAnyAccount)) + ..add(DiagnosticsProperty('hasActiveChat', hasActiveChat)); + } + @override bool operator ==(Object other) { return identical(this, other) || diff --git a/packages/veilid_support/lib/src/config.dart b/packages/veilid_support/lib/src/config.dart index 99860a0..1d2b521 100644 --- a/packages/veilid_support/lib/src/config.dart +++ b/packages/veilid_support/lib/src/config.dart @@ -60,6 +60,13 @@ Future getVeilidConfig(bool isWeb, String appName) async { config.network.routingTable.copyWith(bootstrap: bootstrap))); } + // ignore: do_not_use_environment + const envNetworkKey = String.fromEnvironment('NETWORK_KEY'); + if (envNetworkKey.isNotEmpty) { + config = config.copyWith( + network: config.network.copyWith(networkKeyPassword: envNetworkKey)); + } + // ignore: do_not_use_environment const envBootstrap = String.fromEnvironment('BOOTSTRAP'); if (envBootstrap.isNotEmpty) { From 031d7aea82a4e6007a4b06853f4e3d4fb717fa0a Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Wed, 14 Feb 2024 22:25:02 -0500 Subject: [PATCH 39/68] init fixes --- lib/account_manager/cubits/active_local_account_cubit.dart | 7 +++++++ lib/account_manager/cubits/local_accounts_cubit.dart | 7 +++++++ lib/account_manager/cubits/user_logins_cubit.dart | 7 +++++++ 3 files changed, 21 insertions(+) diff --git a/lib/account_manager/cubits/active_local_account_cubit.dart b/lib/account_manager/cubits/active_local_account_cubit.dart index bc28a92..29a76c9 100644 --- a/lib/account_manager/cubits/active_local_account_cubit.dart +++ b/lib/account_manager/cubits/active_local_account_cubit.dart @@ -3,6 +3,7 @@ import 'dart:async'; import 'package:bloc/bloc.dart'; import 'package:veilid_support/veilid_support.dart'; +import '../../init.dart'; import '../repository/account_repository/account_repository.dart'; class ActiveLocalAccountCubit extends Cubit { @@ -11,6 +12,12 @@ class ActiveLocalAccountCubit extends Cubit { super(null) { // Subscribe to streams _initAccountRepositorySubscription(); + + // Initialize when we can + Future.delayed(Duration.zero, () async { + await eventualInitialized.future; + emit(_accountRepository.getActiveLocalAccount()); + }); } void _initAccountRepositorySubscription() { diff --git a/lib/account_manager/cubits/local_accounts_cubit.dart b/lib/account_manager/cubits/local_accounts_cubit.dart index 457b8ba..376c810 100644 --- a/lib/account_manager/cubits/local_accounts_cubit.dart +++ b/lib/account_manager/cubits/local_accounts_cubit.dart @@ -3,6 +3,7 @@ import 'dart:async'; import 'package:bloc/bloc.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; +import '../../init.dart'; import '../models/models.dart'; import '../repository/account_repository/account_repository.dart'; @@ -12,6 +13,12 @@ class LocalAccountsCubit extends Cubit> { super(IList()) { // Subscribe to streams _initAccountRepositorySubscription(); + + // Initialize when we can + Future.delayed(Duration.zero, () async { + await eventualInitialized.future; + emit(_accountRepository.getLocalAccounts()); + }); } void _initAccountRepositorySubscription() { diff --git a/lib/account_manager/cubits/user_logins_cubit.dart b/lib/account_manager/cubits/user_logins_cubit.dart index 56dbab5..30269c1 100644 --- a/lib/account_manager/cubits/user_logins_cubit.dart +++ b/lib/account_manager/cubits/user_logins_cubit.dart @@ -3,6 +3,7 @@ import 'dart:async'; import 'package:bloc/bloc.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; +import '../../init.dart'; import '../models/models.dart'; import '../repository/account_repository/account_repository.dart'; @@ -12,6 +13,12 @@ class UserLoginsCubit extends Cubit> { super(IList()) { // Subscribe to streams _initAccountRepositorySubscription(); + + // Initialize when we can + Future.delayed(Duration.zero, () async { + await eventualInitialized.future; + emit(_accountRepository.getUserLogins()); + }); } void _initAccountRepositorySubscription() { From fcccacfafa6ec6b7dc0ed70ced13f552fed08a01 Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Thu, 15 Feb 2024 11:05:59 -0700 Subject: [PATCH 40/68] checkpoint --- .../lib/dht_support/src/dht_record.dart | 181 ++++++++++++------ .../lib/dht_support/src/dht_record_cubit.dart | 2 +- .../lib/dht_support/src/dht_record_pool.dart | 2 +- .../lib/dht_support/src/dht_short_array.dart | 30 +-- .../veilid_support/lib/src/json_tools.dart | 9 +- .../lib/src/protobuf_tools.dart | 10 +- packages/veilid_support/pubspec.lock | 2 +- packages/veilid_support/pubspec.yaml | 3 + 8 files changed, 152 insertions(+), 87 deletions(-) diff --git a/packages/veilid_support/lib/dht_support/src/dht_record.dart b/packages/veilid_support/lib/dht_support/src/dht_record.dart index a9064d0..f7f66bd 100644 --- a/packages/veilid_support/lib/dht_support/src/dht_record.dart +++ b/packages/veilid_support/lib/dht_support/src/dht_record.dart @@ -1,12 +1,28 @@ import 'dart:async'; import 'dart:typed_data'; +import 'package:equatable/equatable.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:meta/meta.dart'; import 'package:protobuf/protobuf.dart'; import '../../../veilid_support.dart'; +@immutable +class DHTRecordWatchChange extends Equatable { + const DHTRecordWatchChange( + {required this.local, required this.data, required this.subkeys}); + + final bool local; + final Uint8List data; + final List subkeys; + + @override + List get props => [local, data, subkeys]; +} + +///////////////////////////////////////////////// + class DHTRecord { DHTRecord( {required VeilidRoutingContext routingContext, @@ -34,7 +50,7 @@ class DHTRecord { bool _open; bool _valid; @internal - StreamController? watchController; + StreamController? watchController; @internal bool needsWatchStateUpdate; @internal @@ -160,76 +176,100 @@ class DHTRecord { Future tryWriteBytes(Uint8List newValue, {int subkey = -1}) async { subkey = subkeyOrDefault(subkey); - newValue = await _crypto.encrypt(newValue, subkey); + final lastSeq = _subkeySeqCache[subkey]; + final encryptedNewValue = await _crypto.encrypt(newValue, subkey); // Set the new data if possible - var valueData = await _routingContext.setDHTValue( - _recordDescriptor.key, subkey, newValue); - if (valueData == null) { - // Get the data to check its sequence number - valueData = await _routingContext.getDHTValue( + var newValueData = await _routingContext.setDHTValue( + _recordDescriptor.key, subkey, encryptedNewValue); + if (newValueData == null) { + // A newer value wasn't found on the set, but + // we may get a newer value when getting the value for the sequence number + newValueData = await _routingContext.getDHTValue( _recordDescriptor.key, subkey, false); - assert(valueData != null, "can't get value that was just set"); - _subkeySeqCache[subkey] = valueData!.seq; + if (newValueData == null) { + assert(newValueData != null, "can't get value that was just set"); + return null; + } + } + + // Record new sequence number + final isUpdated = newValueData.seq != lastSeq; + _subkeySeqCache[subkey] = newValueData.seq; + + // See if the encrypted data returned is exactly the same + // if so, shortcut and don't bother decrypting it + if (newValueData.data == encryptedNewValue) { + if (isUpdated) { + addLocalValueChange(newValue, subkey); + } return null; } - _subkeySeqCache[subkey] = valueData.seq; - return valueData.data; + + // Decrypt value to return it + final decryptedNewValue = await _crypto.decrypt(newValueData.data, subkey); + if (isUpdated) { + addLocalValueChange(decryptedNewValue, subkey); + } + return decryptedNewValue; } Future eventualWriteBytes(Uint8List newValue, {int subkey = -1}) async { subkey = subkeyOrDefault(subkey); - newValue = await _crypto.encrypt(newValue, subkey); + final lastSeq = _subkeySeqCache[subkey]; + final encryptedNewValue = await _crypto.encrypt(newValue, subkey); - ValueData? valueData; + ValueData? newValueData; do { - // Set the new data - valueData = await _routingContext.setDHTValue( - _recordDescriptor.key, subkey, newValue); + do { + // Set the new data + newValueData = await _routingContext.setDHTValue( + _recordDescriptor.key, subkey, encryptedNewValue); - // Repeat if newer data on the network was found - } while (valueData != null); + // Repeat if newer data on the network was found + } while (newValueData != null); - // Get the data to check its sequence number - valueData = - await _routingContext.getDHTValue(_recordDescriptor.key, subkey, false); - assert(valueData != null, "can't get value that was just set"); - _subkeySeqCache[subkey] = valueData!.seq; + // Get the data to check its sequence number + newValueData = await _routingContext.getDHTValue( + _recordDescriptor.key, subkey, false); + if (newValueData == null) { + assert(newValueData != null, "can't get value that was just set"); + return; + } + + // Record new sequence number + _subkeySeqCache[subkey] = newValueData.seq; + + // The encrypted data returned should be exactly the same + // as what we are trying to set, + // otherwise we still need to keep trying to set the value + } while (newValueData.data != encryptedNewValue); + + final isUpdated = newValueData.seq != lastSeq; + if (isUpdated) { + addLocalValueChange(newValue, subkey); + } } Future eventualUpdateBytes( - Future Function(Uint8List oldValue) update, + Future Function(Uint8List? oldValue) update, {int subkey = -1}) async { subkey = subkeyOrDefault(subkey); - // Get existing identity key, do not allow force refresh here + + // Get the existing data, do not allow force refresh here // because if we need a refresh the setDHTValue will fail anyway - var valueData = - await _routingContext.getDHTValue(_recordDescriptor.key, subkey, false); - // Ensure it exists already - if (valueData == null) { - throw const FormatException('value does not exist'); - } + var oldValue = + await get(subkey: subkey, forceRefresh: false, onlyUpdates: false); + do { - // Update cache - _subkeySeqCache[subkey] = valueData!.seq; - // Update the data - final oldData = await _crypto.decrypt(valueData.data, subkey); - final updatedData = await update(oldData); - final newData = await _crypto.encrypt(updatedData, subkey); + final updatedValue = await update(oldValue); - // Set it back - valueData = await _routingContext.setDHTValue( - _recordDescriptor.key, subkey, newData); + // Try to write it back to the network + oldValue = await tryWriteBytes(updatedValue, subkey: subkey); - // Repeat if newer data on the network was found - } while (valueData != null); - - // Get the data to check its sequence number - valueData = - await _routingContext.getDHTValue(_recordDescriptor.key, subkey, false); - assert(valueData != null, "can't get value that was just set"); - _subkeySeqCache[subkey] = valueData!.seq; + // Repeat update if newer data on the network was found + } while (oldValue != null); } Future tryWriteJson(T Function(dynamic) fromJson, T newValue, @@ -259,12 +299,12 @@ class DHTRecord { eventualWriteBytes(newValue.writeToBuffer(), subkey: subkey); Future eventualUpdateJson( - T Function(dynamic) fromJson, Future Function(T) update, + T Function(dynamic) fromJson, Future Function(T?) update, {int subkey = -1}) => eventualUpdateBytes(jsonUpdate(fromJson, update), subkey: subkey); Future eventualUpdateProtobuf( - T Function(List) fromBuffer, Future Function(T) update, + T Function(List) fromBuffer, Future Function(T?) update, {int subkey = -1}) => eventualUpdateBytes(protobufUpdate(fromBuffer, update), subkey: subkey); @@ -281,25 +321,34 @@ class DHTRecord { } } - Future> listen( - Future Function( - DHTRecord record, Uint8List data, List subkeys) - onUpdate, - ) async { + Future> listen( + Future Function( + DHTRecord record, Uint8List data, List subkeys) + onUpdate, + {bool localChanges = true}) async { // Set up watch requirements watchController ??= - StreamController.broadcast(onCancel: () { + StreamController.broadcast(onCancel: () { // If there are no more listeners then we can get rid of the controller watchController = null; }); return watchController!.stream.listen( - (update) { + (change) { + if (change.local && !localChanges) { + return; + } Future.delayed(Duration.zero, () async { - final out = await _crypto.decrypt( - update.valueData.data, update.subkeys.first.low); - - await onUpdate(this, out, update.subkeys); + final Uint8List data; + if (change.local) { + // local changes are not encrypted + data = change.data; + } else { + // incoming/remote changes are encrypted + data = + await _crypto.decrypt(change.data, change.subkeys.first.low); + } + await onUpdate(this, data, change.subkeys); }); }, cancelOnError: true, @@ -316,4 +365,14 @@ class DHTRecord { needsWatchStateUpdate = true; } } + + void addLocalValueChange(Uint8List data, int subkey) { + watchController?.add(DHTRecordWatchChange( + local: true, data: data, subkeys: [ValueSubkeyRange.single(subkey)])); + } + + void addRemoteValueChange(VeilidUpdateValueChange update) { + watchController?.add(DHTRecordWatchChange( + local: false, data: update.valueData.data, subkeys: update.subkeys)); + } } diff --git a/packages/veilid_support/lib/dht_support/src/dht_record_cubit.dart b/packages/veilid_support/lib/dht_support/src/dht_record_cubit.dart index 86806cf..9c809b2 100644 --- a/packages/veilid_support/lib/dht_support/src/dht_record_cubit.dart +++ b/packages/veilid_support/lib/dht_support/src/dht_record_cubit.dart @@ -102,7 +102,7 @@ class DHTRecordCubit extends Cubit> { DHTRecord get record => _record; - StreamSubscription? _subscription; + StreamSubscription? _subscription; late DHTRecord _record; bool _wantsCloseRecord; final StateFunction _stateFunction; diff --git a/packages/veilid_support/lib/dht_support/src/dht_record_pool.dart b/packages/veilid_support/lib/dht_support/src/dht_record_pool.dart index 208ac98..7c85d96 100644 --- a/packages/veilid_support/lib/dht_support/src/dht_record_pool.dart +++ b/packages/veilid_support/lib/dht_support/src/dht_record_pool.dart @@ -351,7 +351,7 @@ class DHTRecordPool with TableDBBacked { // Change for (final kv in _opened.entries) { if (kv.key == updateValueChange.key) { - kv.value.watchController?.add(updateValueChange); + kv.value.addRemoteValueChange(updateValueChange); break; } } diff --git a/packages/veilid_support/lib/dht_support/src/dht_short_array.dart b/packages/veilid_support/lib/dht_support/src/dht_short_array.dart index 1f7142e..11af592 100644 --- a/packages/veilid_support/lib/dht_support/src/dht_short_array.dart +++ b/packages/veilid_support/lib/dht_support/src/dht_short_array.dart @@ -63,8 +63,7 @@ class DHTShortArray { _DHTShortArrayCache _head; // Subscription to head and linked record internal changes - final Map> - _subscriptions; + final Map> _subscriptions; // Stream of external changes StreamController? _watchController; // Watch mutex to ensure we keep the representation valid @@ -545,10 +544,10 @@ class DHTShortArray { } final result = await record!.get(subkey: recordSubkey); - if (result != null) { - // A change happened, notify any listeners - _watchController?.sink.add(null); - } + + // A change happened, notify any listeners + _watchController?.sink.add(null); + return result; } on Exception catch (_) { // Exception on write means state needs to be reverted @@ -607,8 +606,8 @@ class DHTShortArray { final recordSubkey = (index % _stride) + ((recordNumber == 0) ? 1 : 0); final result = await record.tryWriteBytes(newValue, subkey: recordSubkey); - if (result != null) { - // A change happened, notify any listeners + if (result == null) { + // A newer value was not found, so the change took _watchController?.sink.add(null); } return result; @@ -625,7 +624,7 @@ class DHTShortArray { } Future eventualUpdateItem( - int pos, Future Function(Uint8List oldValue) update) async { + int pos, Future Function(Uint8List? oldValue) update) async { var oldData = await getItem(pos); // Ensure it exists already if (oldData == null) { @@ -633,7 +632,7 @@ class DHTShortArray { } do { // Update the data - final updatedData = await update(oldData!); + final updatedData = await update(oldData); // Set it back oldData = await tryWriteItem(pos, updatedData); @@ -673,14 +672,14 @@ class DHTShortArray { Future eventualUpdateItemJson( T Function(dynamic) fromJson, int pos, - Future Function(T) update, + Future Function(T?) update, ) => eventualUpdateItem(pos, jsonUpdate(fromJson, update)); Future eventualUpdateItemProtobuf( T Function(List) fromBuffer, int pos, - Future Function(T) update, + Future Function(T?) update, ) => eventualUpdateItem(pos, protobufUpdate(fromBuffer, update)); @@ -692,14 +691,17 @@ class DHTShortArray { .wait; // Update changes to the head record + // Don't watch for local changes because this class already handles + // notifying listeners and knows when it makes local changes if (!_subscriptions.containsKey(_headRecord.key)) { _subscriptions[_headRecord.key] = - await _headRecord.listen(_onUpdateRecord); + await _headRecord.listen(localChanges: false, _onUpdateRecord); } // Update changes to any linked records for (final lr in _head.linkedRecords) { if (!_subscriptions.containsKey(lr.key)) { - _subscriptions[lr.key] = await lr.listen(_onUpdateRecord); + _subscriptions[lr.key] = + await lr.listen(localChanges: false, _onUpdateRecord); } } } on Exception { diff --git a/packages/veilid_support/lib/src/json_tools.dart b/packages/veilid_support/lib/src/json_tools.dart index e7bfd09..c5895d0 100644 --- a/packages/veilid_support/lib/src/json_tools.dart +++ b/packages/veilid_support/lib/src/json_tools.dart @@ -13,14 +13,15 @@ Uint8List jsonEncodeBytes(Object? object, utf8.encode(jsonEncode(object, toEncodable: toEncodable))); Future jsonUpdateBytes(T Function(dynamic) fromJson, - Uint8List oldBytes, Future Function(T) update) async { - final oldObj = fromJson(jsonDecode(utf8.decode(oldBytes))); + Uint8List? oldBytes, Future Function(T?) update) async { + final oldObj = + oldBytes == null ? null : fromJson(jsonDecode(utf8.decode(oldBytes))); final newObj = await update(oldObj); return jsonEncodeBytes(newObj); } -Future Function(Uint8List) jsonUpdate( - T Function(dynamic) fromJson, Future Function(T) update) => +Future Function(Uint8List?) jsonUpdate( + T Function(dynamic) fromJson, Future Function(T?) update) => (oldBytes) => jsonUpdateBytes(fromJson, oldBytes, update); T Function(Object?) genericFromJson( diff --git a/packages/veilid_support/lib/src/protobuf_tools.dart b/packages/veilid_support/lib/src/protobuf_tools.dart index c24302c..94dc6d1 100644 --- a/packages/veilid_support/lib/src/protobuf_tools.dart +++ b/packages/veilid_support/lib/src/protobuf_tools.dart @@ -4,14 +4,14 @@ import 'package:protobuf/protobuf.dart'; Future protobufUpdateBytes( T Function(List) fromBuffer, - Uint8List oldBytes, - Future Function(T) update) async { - final oldObj = fromBuffer(oldBytes); + Uint8List? oldBytes, + Future Function(T?) update) async { + final oldObj = oldBytes == null ? null : fromBuffer(oldBytes); final newObj = await update(oldObj); return Uint8List.fromList(newObj.writeToBuffer()); } -Future Function(Uint8List) +Future Function(Uint8List?) protobufUpdate( - T Function(List) fromBuffer, Future Function(T) update) => + T Function(List) fromBuffer, Future Function(T?) update) => (oldBytes) => protobufUpdateBytes(fromBuffer, oldBytes, update); diff --git a/packages/veilid_support/pubspec.lock b/packages/veilid_support/pubspec.lock index 54c4755..3ce09e2 100644 --- a/packages/veilid_support/pubspec.lock +++ b/packages/veilid_support/pubspec.lock @@ -411,7 +411,7 @@ packages: source: hosted version: "1.0.4" mutex: - dependency: transitive + dependency: "direct main" description: path: "../mutex" relative: true diff --git a/packages/veilid_support/pubspec.yaml b/packages/veilid_support/pubspec.yaml index b1d21c1..acbe5af 100644 --- a/packages/veilid_support/pubspec.yaml +++ b/packages/veilid_support/pubspec.yaml @@ -16,6 +16,9 @@ dependencies: json_annotation: ^4.8.1 loggy: ^2.0.3 meta: ^1.10.0 + mutex: + path: ../mutex + protobuf: ^3.0.0 veilid: # veilid: ^0.0.1 From f936cb069e10c6907ea2d6c557e19ca8fbd42b67 Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Fri, 16 Feb 2024 09:46:42 -0700 Subject: [PATCH 41/68] cleanup --- .../account_repository.dart | 17 +++--- .../lib/dht_support/src/dht_record.dart | 4 +- packages/veilid_support/lib/src/identity.dart | 52 +++++++++++++++---- packages/veilid_support/lib/src/table_db.dart | 14 ++--- .../veilid_support/lib/veilid_support.dart | 1 + 5 files changed, 61 insertions(+), 27 deletions(-) diff --git a/lib/account_manager/repository/account_repository/account_repository.dart b/lib/account_manager/repository/account_repository/account_repository.dart index 0ce8ce7..40ce315 100644 --- a/lib/account_manager/repository/account_repository/account_repository.dart +++ b/lib/account_manager/repository/account_repository/account_repository.dart @@ -299,17 +299,18 @@ class AccountRepository { Future _decryptedLogin( IdentityMaster identityMaster, SecretKey identitySecret) async { - final cs = await Veilid.instance - .getCryptoSystem(identityMaster.identityRecordKey.kind); - final keyOk = await cs.validateKeyPair( - identityMaster.identityPublicKey, identitySecret); - if (!keyOk) { - throw Exception('Identity is corrupted'); - } + // Verify identity secret works and return the valid cryptosystem + final cs = await identityMaster.validateIdentitySecret(identitySecret); // Read the identity key to get the account keys - final accountRecordInfo = await identityMaster.readAccountFromIdentity( + final accountRecordInfoList = await identityMaster.readAccountsFromIdentity( identitySecret: identitySecret, accountKey: veilidChatAccountKey); + if (accountRecordInfoList.length > 1) { + throw IdentityException.limitExceeded; + } else if (accountRecordInfoList.isEmpty) { + throw IdentityException.noAccount; + } + final accountRecordInfo = accountRecordInfoList.single; // Add to user logins and select it final userLogins = await _userLogins.get(); diff --git a/packages/veilid_support/lib/dht_support/src/dht_record.dart b/packages/veilid_support/lib/dht_support/src/dht_record.dart index f7f66bd..d9a1337 100644 --- a/packages/veilid_support/lib/dht_support/src/dht_record.dart +++ b/packages/veilid_support/lib/dht_support/src/dht_record.dart @@ -199,7 +199,7 @@ class DHTRecord { // See if the encrypted data returned is exactly the same // if so, shortcut and don't bother decrypting it - if (newValueData.data == encryptedNewValue) { + if (newValueData.data.equals(encryptedNewValue)) { if (isUpdated) { addLocalValueChange(newValue, subkey); } @@ -243,7 +243,7 @@ class DHTRecord { // The encrypted data returned should be exactly the same // as what we are trying to set, // otherwise we still need to keep trying to set the value - } while (newValueData.data != encryptedNewValue); + } while (!newValueData.data.equals(encryptedNewValue)); final isUpdated = newValueData.seq != lastSeq; if (isUpdated) { diff --git a/packages/veilid_support/lib/src/identity.dart b/packages/veilid_support/lib/src/identity.dart index 5a77e2e..70dc295 100644 --- a/packages/veilid_support/lib/src/identity.dart +++ b/packages/veilid_support/lib/src/identity.dart @@ -10,6 +10,20 @@ import '../dht_support/dht_support.dart'; part 'identity.freezed.dart'; part 'identity.g.dart'; +// Identity errors +enum IdentityException implements Exception { + readError('identity could not be read'), + noAccount('no account record info'), + limitExceeded('too many items for the limit'), + invalid('identity is corrupted or secret is invalid'); + + const IdentityException(this.message); + final String message; + + @override + String toString() => 'IdentityException($name): $message'; +} + // AccountOwnerInfo is the key and owner info for the account dht key that is // stored in the identity key @freezed @@ -82,6 +96,12 @@ extension IdentityMasterExtension on IdentityMaster { await (await pool.openRead(masterRecordKey)).delete(); } + Future get identityCrypto => + Veilid.instance.getCryptoSystem(identityRecordKey.kind); + + Future get masterCrypto => + Veilid.instance.getCryptoSystem(masterRecordKey.kind); + KeyPair identityWriter(SecretKey secret) => KeyPair(key: identityPublicKey, secret: secret); @@ -91,7 +111,17 @@ extension IdentityMasterExtension on IdentityMaster { TypedKey identityPublicTypedKey() => TypedKey(kind: identityRecordKey.kind, value: identityPublicKey); - Future readAccountFromIdentity( + Future validateIdentitySecret( + SecretKey identitySecret) async { + final cs = await identityCrypto; + final keyOk = await cs.validateKeyPair(identityPublicKey, identitySecret); + if (!keyOk) { + throw IdentityException.invalid; + } + return cs; + } + + Future> readAccountsFromIdentity( {required SharedSecret identitySecret, required String accountKey}) async { // Read the identity key to get the account keys @@ -100,23 +130,19 @@ extension IdentityMasterExtension on IdentityMaster { final identityRecordCrypto = await DHTRecordCryptoPrivate.fromSecret( identityRecordKey.kind, identitySecret); - late final AccountRecordInfo accountRecordInfo; + late final List accountRecordInfo; await (await pool.openRead(identityRecordKey, parent: masterRecordKey, crypto: identityRecordCrypto)) .scope((identityRec) async { final identity = await identityRec.getJson(Identity.fromJson); if (identity == null) { // Identity could not be read or decrypted from DHT - throw StateError('identity could not be read'); + throw IdentityException.readError; } final accountRecords = IMapOfSets.from(identity.accountRecords); final vcAccounts = accountRecords.get(accountKey); - if (vcAccounts.length != 1) { - // No account, or multiple accounts somehow associated with identity - throw StateError('no single account record info'); - } - accountRecordInfo = vcAccounts.first; + accountRecordInfo = vcAccounts.toList(); }); return accountRecordInfo; @@ -128,6 +154,7 @@ extension IdentityMasterExtension on IdentityMaster { required SharedSecret identitySecret, required String accountKey, required Future Function(TypedKey parent) createAccountCallback, + int maxAccounts = 1, }) async { final pool = DHTRecordPool.instance; @@ -153,11 +180,14 @@ extension IdentityMasterExtension on IdentityMaster { await identityRec.eventualUpdateJson(Identity.fromJson, (oldIdentity) async { + if (oldIdentity == null) { + throw IdentityException.readError; + } final oldAccountRecords = IMapOfSets.from(oldIdentity.accountRecords); - // Only allow one account per identity for veilidchat - if (oldAccountRecords.get(accountKey).isNotEmpty) { - throw StateError('Only one account per key in identity'); + + if (oldAccountRecords.get(accountKey).length >= maxAccounts) { + throw IdentityException.limitExceeded; } final accountRecords = oldAccountRecords .add(accountKey, newAccountRecordInfo) diff --git a/packages/veilid_support/lib/src/table_db.dart b/packages/veilid_support/lib/src/table_db.dart index f6a69b7..1e09fc4 100644 --- a/packages/veilid_support/lib/src/table_db.dart +++ b/packages/veilid_support/lib/src/table_db.dart @@ -1,5 +1,6 @@ import 'dart:async'; +import 'package:async_tools/async_tools.dart'; import 'package:veilid/veilid.dart'; Future tableScope( @@ -67,25 +68,26 @@ class TableDBValue extends TableDBBacked { _tableKeyName = tableKeyName, _streamController = StreamController.broadcast(); - T? get value => _value; - T get requireValue => _value!; + AsyncData? get value => _value; + T get requireValue => _value!.value; Stream get stream => _streamController.stream; Future get() async { final val = _value; if (val != null) { - return val; + return val.value; } final loadedValue = await load(); - return _value = loadedValue; + _value = AsyncData(loadedValue); + return loadedValue; } Future set(T newVal) async { - _value = await store(newVal); + _value = AsyncData(await store(newVal)); _streamController.add(newVal); } - T? _value; + AsyncData? _value; final String _tableName; final String _tableKeyName; final T Function(Object? obj) _valueFromJson; diff --git a/packages/veilid_support/lib/veilid_support.dart b/packages/veilid_support/lib/veilid_support.dart index f873397..f9e9293 100644 --- a/packages/veilid_support/lib/veilid_support.dart +++ b/packages/veilid_support/lib/veilid_support.dart @@ -9,6 +9,7 @@ export 'dht_support/dht_support.dart'; export 'src/config.dart'; export 'src/identity.dart'; export 'src/json_tools.dart'; +export 'src/memory_tools.dart'; export 'src/protobuf_tools.dart'; export 'src/table_db.dart'; export 'src/veilid_log.dart'; From 450bdf9c7c872beec0c6df57f82c59af5faa6877 Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Tue, 20 Feb 2024 17:57:05 -0500 Subject: [PATCH 42/68] state follower --- lib/chat/cubits/active_chat_cubit.dart | 9 +- lib/chat/views/chat_component.dart | 2 +- .../active_conversation_messages_cubit.dart | 2 +- ... active_conversations_bloc_map_cubit.dart} | 5 +- lib/chat_list/cubits/cubits.dart | 2 +- .../chat_single_contact_item_widget.dart | 2 +- .../cubits/contact_invitation_list_cubit.dart | 188 +++++++-------- .../cubits/contact_request_inbox_cubit.dart | 151 ++++++++++++ lib/contact_invitation/cubits/cubits.dart | 3 + .../cubits/waiting_invitation_cubit.dart | 221 ++++++++++++++++++ .../waiting_invitations_bloc_map_cubit.dart | 58 +++++ .../models/accepted_contact.dart | 11 +- lib/contacts/views/contact_item_widget.dart | 15 -- .../home_account_ready_shell.dart | 11 +- lib/tools/async_transformer_cubit.dart | 62 +++++ lib/tools/bloc_map_cubit.dart | 8 + lib/tools/bloc_tools.dart | 12 + lib/tools/state_follower.dart | 78 +++++++ lib/tools/tools.dart | 3 + .../veilid_support/lib/src/memory_tools.dart | 72 ++++++ 20 files changed, 787 insertions(+), 128 deletions(-) rename lib/chat_list/cubits/{active_conversations_cubit.dart => active_conversations_bloc_map_cubit.dart} (94%) create mode 100644 lib/contact_invitation/cubits/contact_request_inbox_cubit.dart create mode 100644 lib/contact_invitation/cubits/waiting_invitation_cubit.dart create mode 100644 lib/contact_invitation/cubits/waiting_invitations_bloc_map_cubit.dart create mode 100644 lib/tools/async_transformer_cubit.dart create mode 100644 lib/tools/bloc_tools.dart create mode 100644 lib/tools/state_follower.dart create mode 100644 packages/veilid_support/lib/src/memory_tools.dart diff --git a/lib/chat/cubits/active_chat_cubit.dart b/lib/chat/cubits/active_chat_cubit.dart index 6215098..fa88d56 100644 --- a/lib/chat/cubits/active_chat_cubit.dart +++ b/lib/chat/cubits/active_chat_cubit.dart @@ -1,13 +1,12 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:veilid_support/veilid_support.dart'; -class ActiveChatCubit extends Cubit { - ActiveChatCubit(super.initialState, this.setHasActiveChat); +import '../../tools/tools.dart'; + +class ActiveChatCubit extends Cubit with BlocTools { + ActiveChatCubit(super.initialState); void setActiveChat(TypedKey? activeChatRemoteConversationRecordKey) { - setHasActiveChat(activeChatRemoteConversationRecordKey != null); emit(activeChatRemoteConversationRecordKey); } - - void Function(bool) setHasActiveChat; } diff --git a/lib/chat/views/chat_component.dart b/lib/chat/views/chat_component.dart index ba866c4..8e80e15 100644 --- a/lib/chat/views/chat_component.dart +++ b/lib/chat/views/chat_component.dart @@ -52,7 +52,7 @@ class ChatComponent extends StatelessWidget { if (contactList == null) { return debugPage('should always have a contact list here'); } - final avconversation = context.select?>( (x) => x.state[remoteConversationRecordKey]); if (avconversation == null) { diff --git a/lib/chat_list/cubits/active_conversation_messages_cubit.dart b/lib/chat_list/cubits/active_conversation_messages_cubit.dart index bb6209c..aad822d 100644 --- a/lib/chat_list/cubits/active_conversation_messages_cubit.dart +++ b/lib/chat_list/cubits/active_conversation_messages_cubit.dart @@ -8,7 +8,7 @@ import '../../account_manager/account_manager.dart'; import '../../chat/chat.dart'; import '../../proto/proto.dart' as proto; import '../../tools/tools.dart'; -import 'active_conversations_cubit.dart'; +import 'active_conversations_bloc_map_cubit.dart'; class ActiveConversationMessagesCubit extends BlocMapCubit>, MessagesCubit> { diff --git a/lib/chat_list/cubits/active_conversations_cubit.dart b/lib/chat_list/cubits/active_conversations_bloc_map_cubit.dart similarity index 94% rename from lib/chat_list/cubits/active_conversations_cubit.dart rename to lib/chat_list/cubits/active_conversations_bloc_map_cubit.dart index dccd9c9..946ec99 100644 --- a/lib/chat_list/cubits/active_conversations_cubit.dart +++ b/lib/chat_list/cubits/active_conversations_bloc_map_cubit.dart @@ -32,9 +32,10 @@ typedef ActiveConversationsBlocMapState // Map of remoteConversationRecordKey to ActiveConversationCubit // Wraps a conversation cubit to only expose completely built conversations -class ActiveConversationsCubit extends BlocMapCubit, ActiveConversationCubit> { - ActiveConversationsCubit({required ActiveAccountInfo activeAccountInfo}) + ActiveConversationsBlocMapCubit( + {required ActiveAccountInfo activeAccountInfo}) : _activeAccountInfo = activeAccountInfo; // Add an active conversation to be tracked for changes diff --git a/lib/chat_list/cubits/cubits.dart b/lib/chat_list/cubits/cubits.dart index 474f5cb..7ff0db1 100644 --- a/lib/chat_list/cubits/cubits.dart +++ b/lib/chat_list/cubits/cubits.dart @@ -1,3 +1,3 @@ export 'active_conversation_messages_cubit.dart'; -export 'active_conversations_cubit.dart'; +export 'active_conversations_bloc_map_cubit.dart'; export 'chat_list_cubit.dart'; diff --git a/lib/chat_list/views/chat_single_contact_item_widget.dart b/lib/chat_list/views/chat_single_contact_item_widget.dart index 64f5a1f..5bb39ee 100644 --- a/lib/chat_list/views/chat_single_contact_item_widget.dart +++ b/lib/chat_list/views/chat_single_contact_item_widget.dart @@ -69,7 +69,7 @@ class ChatSingleContactItemWidget extends StatelessWidget { child: ListTile( onTap: () { final activeConversationsCubit = - context.read(); + context.read(); singleFuture(activeChatCubit, () async { await activeConversationsCubit.addConversation( contact: _contact); diff --git a/lib/contact_invitation/cubits/contact_invitation_list_cubit.dart b/lib/contact_invitation/cubits/contact_invitation_list_cubit.dart index 9c484a7..429be53 100644 --- a/lib/contact_invitation/cubits/contact_invitation_list_cubit.dart +++ b/lib/contact_invitation/cubits/contact_invitation_list_cubit.dart @@ -5,9 +5,7 @@ import 'package:flutter/foundation.dart'; import 'package:veilid_support/veilid_support.dart'; import '../../account_manager/account_manager.dart'; -import '../../contacts/contacts.dart'; import '../../proto/proto.dart' as proto; -import '../../tools/tools.dart'; import '../models/models.dart'; ////////////////////////////////////////////////// @@ -22,12 +20,6 @@ typedef GetEncryptionKeyCallback = Future Function( EncryptionKeyType encryptionKeyType, Uint8List encryptedSecret); -@immutable -class InvitationStatus { - const InvitationStatus({required this.acceptedContact}); - final AcceptedContact? acceptedContact; -} - ////////////////////////////////////////////////// ////////////////////////////////////////////////// @@ -271,109 +263,109 @@ class ContactInvitationListCubit return out; } - Future checkInvitationStatus( - {required proto.ContactInvitationRecord contactInvitationRecord}) async { - // Open the contact request inbox - try { - final pool = DHTRecordPool.instance; - final accountRecordKey = _activeAccountInfo - .userLogin.accountRecordInfo.accountRecord.recordKey; - final writerKey = contactInvitationRecord.writerKey.toVeilid(); - final writerSecret = contactInvitationRecord.writerSecret.toVeilid(); - final recordKey = - contactInvitationRecord.contactRequestInbox.recordKey.toVeilid(); - final writer = TypedKeyPair( - kind: recordKey.kind, key: writerKey, secret: writerSecret); - final acceptReject = await (await pool.openRead(recordKey, - crypto: await DHTRecordCryptoPrivate.fromTypedKeyPair(writer), - parent: accountRecordKey, - defaultSubkey: 1)) - .scope((contactRequestInbox) async { - // - final signedContactResponse = await contactRequestInbox.getProtobuf( - proto.SignedContactResponse.fromBuffer, - forceRefresh: true); - if (signedContactResponse == null) { - return null; - } + // Future checkInvitationStatus( + // {required proto.ContactInvitationRecord contactInvitationRecord}) async { + // // Open the contact request inbox + // try { + // final pool = DHTRecordPool.instance; + // final accountRecordKey = _activeAccountInfo + // .userLogin.accountRecordInfo.accountRecord.recordKey; + // final writerKey = contactInvitationRecord.writerKey.toVeilid(); + // final writerSecret = contactInvitationRecord.writerSecret.toVeilid(); + // final recordKey = + // contactInvitationRecord.contactRequestInbox.recordKey.toVeilid(); + // final writer = TypedKeyPair( + // kind: recordKey.kind, key: writerKey, secret: writerSecret); + // final acceptReject = await (await pool.openRead(recordKey, + // crypto: await DHTRecordCryptoPrivate.fromTypedKeyPair(writer), + // parent: accountRecordKey, + // defaultSubkey: 1)) + // .scope((contactRequestInbox) async { + // // + // final signedContactResponse = await contactRequestInbox.getProtobuf( + // proto.SignedContactResponse.fromBuffer, + // forceRefresh: true); + // if (signedContactResponse == null) { + // return null; + // } - final contactResponseBytes = - Uint8List.fromList(signedContactResponse.contactResponse); - final contactResponse = - proto.ContactResponse.fromBuffer(contactResponseBytes); - final contactIdentityMasterRecordKey = - contactResponse.identityMasterRecordKey.toVeilid(); - final cs = await pool.veilid.getCryptoSystem(recordKey.kind); + // final contactResponseBytes = + // Uint8List.fromList(signedContactResponse.contactResponse); + // final contactResponse = + // proto.ContactResponse.fromBuffer(contactResponseBytes); + // final contactIdentityMasterRecordKey = + // contactResponse.identityMasterRecordKey.toVeilid(); + // final cs = await pool.veilid.getCryptoSystem(recordKey.kind); - // Fetch the remote contact's account master - final contactIdentityMaster = await openIdentityMaster( - identityMasterRecordKey: contactIdentityMasterRecordKey); + // // Fetch the remote contact's account master + // final contactIdentityMaster = await openIdentityMaster( + // identityMasterRecordKey: contactIdentityMasterRecordKey); - // Verify - final signature = signedContactResponse.identitySignature.toVeilid(); - await cs.verify(contactIdentityMaster.identityPublicKey, - contactResponseBytes, signature); + // // Verify + // final signature = signedContactResponse.identitySignature.toVeilid(); + // await cs.verify(contactIdentityMaster.identityPublicKey, + // contactResponseBytes, signature); - // Check for rejection - if (!contactResponse.accept) { - return const InvitationStatus(acceptedContact: null); - } + // // Check for rejection + // if (!contactResponse.accept) { + // return const InvitationStatus(acceptedContact: null); + // } - // Pull profile from remote conversation key - final remoteConversationRecordKey = - contactResponse.remoteConversationRecordKey.toVeilid(); + // // Pull profile from remote conversation key + // final remoteConversationRecordKey = + // contactResponse.remoteConversationRecordKey.toVeilid(); - final conversation = ConversationCubit( - activeAccountInfo: _activeAccountInfo, - remoteIdentityPublicKey: - contactIdentityMaster.identityPublicTypedKey(), - remoteConversationRecordKey: remoteConversationRecordKey); - await conversation.refresh(); + // final conversation = ConversationCubit( + // activeAccountInfo: _activeAccountInfo, + // remoteIdentityPublicKey: + // contactIdentityMaster.identityPublicTypedKey(), + // remoteConversationRecordKey: remoteConversationRecordKey); + // await conversation.refresh(); - final remoteConversation = - conversation.state.data?.value.remoteConversation; - if (remoteConversation == null) { - log.info('Remote conversation could not be read. Waiting...'); - return null; - } + // final remoteConversation = + // conversation.state.data?.value.remoteConversation; + // if (remoteConversation == null) { + // log.info('Remote conversation could not be read. Waiting...'); + // return null; + // } - // Complete the local conversation now that we have the remote profile - final localConversationRecordKey = - contactInvitationRecord.localConversationRecordKey.toVeilid(); - return conversation.initLocalConversation( - existingConversationRecordKey: localConversationRecordKey, - profile: _account.profile, - // ignore: prefer_expression_function_bodies - callback: (localConversation) async { - return InvitationStatus( - acceptedContact: AcceptedContact( - remoteProfile: remoteConversation.profile, - remoteIdentity: contactIdentityMaster, - remoteConversationRecordKey: remoteConversationRecordKey, - localConversationRecordKey: localConversationRecordKey)); - }); - }); + // // Complete the local conversation now that we have the remote profile + // final localConversationRecordKey = + // contactInvitationRecord.localConversationRecordKey.toVeilid(); + // return conversation.initLocalConversation( + // existingConversationRecordKey: localConversationRecordKey, + // profile: _account.profile, + // // ignore: prefer_expression_function_bodies + // callback: (localConversation) async { + // return InvitationStatus( + // acceptedContact: AcceptedContact( + // remoteProfile: remoteConversation.profile, + // remoteIdentity: contactIdentityMaster, + // remoteConversationRecordKey: remoteConversationRecordKey, + // localConversationRecordKey: localConversationRecordKey)); + // }); + // }); - if (acceptReject == null) { - return null; - } + // if (acceptReject == null) { + // return null; + // } - // Delete invitation and return the accepted or rejected contact - await deleteInvitation( - accepted: acceptReject.acceptedContact != null, - contactInvitationRecord: contactInvitationRecord); + // // Delete invitation and return the accepted or rejected contact + // await deleteInvitation( + // accepted: acceptReject.acceptedContact != null, + // contactInvitationRecord: contactInvitationRecord); - return acceptReject; - } on Exception catch (e) { - log.error('Exception in checkAcceptRejectContact: $e', e); + // return acceptReject; + // } on Exception catch (e) { + // log.error('Exception in checkInvitationStatus: $e', e); - // Attempt to clean up. All this needs better lifetime management - await deleteInvitation( - accepted: false, contactInvitationRecord: contactInvitationRecord); + // // Attempt to clean up. All this needs better lifetime management + // await deleteInvitation( + // accepted: false, contactInvitationRecord: contactInvitationRecord); - rethrow; - } - } + // rethrow; + // } + // } // final ActiveAccountInfo _activeAccountInfo; diff --git a/lib/contact_invitation/cubits/contact_request_inbox_cubit.dart b/lib/contact_invitation/cubits/contact_request_inbox_cubit.dart new file mode 100644 index 0000000..eea29ec --- /dev/null +++ b/lib/contact_invitation/cubits/contact_request_inbox_cubit.dart @@ -0,0 +1,151 @@ +import 'package:veilid_support/veilid_support.dart'; + +import '../../account_manager/account_manager.dart'; +import '../../proto/proto.dart' as proto; + +class ContactRequestInboxCubit + extends DefaultDHTRecordCubit { + ContactRequestInboxCubit( + {required this.activeAccountInfo, required this.contactInvitationRecord}) + : super( + open: () => _open( + activeAccountInfo: activeAccountInfo, + contactInvitationRecord: contactInvitationRecord), + decodeState: proto.SignedContactResponse.fromBuffer); + + ContactRequestInboxCubit.value( + {required super.record, + required this.activeAccountInfo, + required this.contactInvitationRecord}) + : super.value(decodeState: proto.SignedContactResponse.fromBuffer); + + static Future _open( + {required ActiveAccountInfo activeAccountInfo, + required proto.ContactInvitationRecord contactInvitationRecord}) async { + final pool = DHTRecordPool.instance; + final accountRecordKey = + activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey; + final writerKey = contactInvitationRecord.writerKey.toVeilid(); + final writerSecret = contactInvitationRecord.writerSecret.toVeilid(); + final recordKey = + contactInvitationRecord.contactRequestInbox.recordKey.toVeilid(); + final writer = TypedKeyPair( + kind: recordKey.kind, key: writerKey, secret: writerSecret); + return pool.openRead(recordKey, + crypto: await DHTRecordCryptoPrivate.fromTypedKeyPair(writer), + parent: accountRecordKey, + defaultSubkey: 1); + } + + final ActiveAccountInfo activeAccountInfo; + final proto.ContactInvitationRecord contactInvitationRecord; +} + // Future checkInvitationStatus( + // {}) async { + // // Open the contact request inbox + // try { + // final pool = DHTRecordPool.instance; + // final accountRecordKey = _activeAccountInfo + // .userLogin.accountRecordInfo.accountRecord.recordKey; + // final writerKey = contactInvitationRecord.writerKey.toVeilid(); + // final writerSecret = contactInvitationRecord.writerSecret.toVeilid(); + // final recordKey = + // contactInvitationRecord.contactRequestInbox.recordKey.toVeilid(); + // final writer = TypedKeyPair( + // kind: recordKey.kind, key: writerKey, secret: writerSecret); + // final acceptReject = await (await pool.openRead(recordKey, + // crypto: await DHTRecordCryptoPrivate.fromTypedKeyPair(writer), + // parent: accountRecordKey, + // defaultSubkey: 1)) + // .scope((contactRequestInbox) async { + // // + // final signedContactResponse = await contactRequestInbox.getProtobuf( + // proto.SignedContactResponse.fromBuffer, + // forceRefresh: true); + // if (signedContactResponse == null) { + // return null; + // } + + // final contactResponseBytes = + // Uint8List.fromList(signedContactResponse.contactResponse); + // final contactResponse = + // proto.ContactResponse.fromBuffer(contactResponseBytes); + // final contactIdentityMasterRecordKey = + // contactResponse.identityMasterRecordKey.toVeilid(); + // final cs = await pool.veilid.getCryptoSystem(recordKey.kind); + + // // Fetch the remote contact's account master + // final contactIdentityMaster = await openIdentityMaster( + // identityMasterRecordKey: contactIdentityMasterRecordKey); + + // // Verify + // final signature = signedContactResponse.identitySignature.toVeilid(); + // await cs.verify(contactIdentityMaster.identityPublicKey, + // contactResponseBytes, signature); + + // // Check for rejection + // if (!contactResponse.accept) { + // return const InvitationStatus(acceptedContact: null); + // } + + // // Pull profile from remote conversation key + // final remoteConversationRecordKey = + // contactResponse.remoteConversationRecordKey.toVeilid(); + + // final conversation = ConversationCubit( + // activeAccountInfo: _activeAccountInfo, + // remoteIdentityPublicKey: + // contactIdentityMaster.identityPublicTypedKey(), + // remoteConversationRecordKey: remoteConversationRecordKey); + // await conversation.refresh(); + + // final remoteConversation = + // conversation.state.data?.value.remoteConversation; + // if (remoteConversation == null) { + // log.info('Remote conversation could not be read. Waiting...'); + // return null; + // } + + // // Complete the local conversation now that we have the remote profile + // final localConversationRecordKey = + // contactInvitationRecord.localConversationRecordKey.toVeilid(); + // return conversation.initLocalConversation( + // existingConversationRecordKey: localConversationRecordKey, + // profile: _account.profile, + // // ignore: prefer_expression_function_bodies + // callback: (localConversation) async { + // return InvitationStatus( + // acceptedContact: AcceptedContact( + // remoteProfile: remoteConversation.profile, + // remoteIdentity: contactIdentityMaster, + // remoteConversationRecordKey: remoteConversationRecordKey, + // localConversationRecordKey: localConversationRecordKey)); + // }); + // }); + + // if (acceptReject == null) { + // return null; + // } + + // // Delete invitation and return the accepted or rejected contact + // await deleteInvitation( + // accepted: acceptReject.acceptedContact != null, + // contactInvitationRecord: contactInvitationRecord); + + // return acceptReject; + // } on Exception catch (e) { + // log.error('Exception in checkInvitationStatus: $e', e); + + // // Attempt to clean up. All this needs better lifetime management + // await deleteInvitation( + // accepted: false, contactInvitationRecord: contactInvitationRecord); + + // rethrow; + // } + + + + + + + diff --git a/lib/contact_invitation/cubits/cubits.dart b/lib/contact_invitation/cubits/cubits.dart index c16213a..c55e119 100644 --- a/lib/contact_invitation/cubits/cubits.dart +++ b/lib/contact_invitation/cubits/cubits.dart @@ -1 +1,4 @@ export 'contact_invitation_list_cubit.dart'; +export 'contact_request_inbox_cubit.dart'; +export 'waiting_invitation_cubit.dart'; +export 'waiting_invitations_bloc_map_cubit.dart'; diff --git a/lib/contact_invitation/cubits/waiting_invitation_cubit.dart b/lib/contact_invitation/cubits/waiting_invitation_cubit.dart new file mode 100644 index 0000000..44e0f36 --- /dev/null +++ b/lib/contact_invitation/cubits/waiting_invitation_cubit.dart @@ -0,0 +1,221 @@ +import 'dart:typed_data'; + +import 'package:async_tools/async_tools.dart'; +import 'package:equatable/equatable.dart'; +import 'package:meta/meta.dart'; +import 'package:veilid_support/veilid_support.dart'; + +import '../../account_manager/account_manager.dart'; +import '../../contacts/contacts.dart'; +import '../../proto/proto.dart' as proto; +import '../../tools/tools.dart'; +import '../models/accepted_contact.dart'; +import 'contact_request_inbox_cubit.dart'; + +@immutable +class InvitationStatus extends Equatable { + const InvitationStatus({required this.acceptedContact}); + final AcceptedContact? acceptedContact; + + @override + List get props => [acceptedContact]; +} + +class WaitingInvitationCubit extends AsyncTransformerCubit { + WaitingInvitationCubit(ContactRequestInboxCubit super.input, + {required ActiveAccountInfo activeAccountInfo, + required proto.Account account, + required proto.ContactInvitationRecord contactInvitationRecord}) + : super( + transform: (signedContactResponse) => _transform( + signedContactResponse, + activeAccountInfo: activeAccountInfo, + account: account, + contactInvitationRecord: contactInvitationRecord)); + + static Future> _transform( + proto.SignedContactResponse signedContactResponse, + {required ActiveAccountInfo activeAccountInfo, + required proto.Account account, + required proto.ContactInvitationRecord contactInvitationRecord}) async { + final pool = DHTRecordPool.instance; + final contactResponseBytes = + Uint8List.fromList(signedContactResponse.contactResponse); + final contactResponse = + proto.ContactResponse.fromBuffer(contactResponseBytes); + final contactIdentityMasterRecordKey = + contactResponse.identityMasterRecordKey.toVeilid(); + final cs = + await pool.veilid.getCryptoSystem(contactIdentityMasterRecordKey.kind); + + // Fetch the remote contact's account master + final contactIdentityMaster = await openIdentityMaster( + identityMasterRecordKey: contactIdentityMasterRecordKey); + + // Verify + final signature = signedContactResponse.identitySignature.toVeilid(); + await cs.verify(contactIdentityMaster.identityPublicKey, + contactResponseBytes, signature); + + // Check for rejection + if (!contactResponse.accept) { + // Rejection + return const AsyncValue.data(InvitationStatus(acceptedContact: null)); + } + + // Pull profile from remote conversation key + final remoteConversationRecordKey = + contactResponse.remoteConversationRecordKey.toVeilid(); + + final conversation = ConversationCubit( + activeAccountInfo: activeAccountInfo, + remoteIdentityPublicKey: contactIdentityMaster.identityPublicTypedKey(), + remoteConversationRecordKey: remoteConversationRecordKey); + + // wait for remote conversation for up to 20 seconds + proto.Conversation? remoteConversation; + var retryCount = 20; + do { + await conversation.refresh(); + remoteConversation = conversation.state.data?.value.remoteConversation; + if (remoteConversation != null) { + break; + } + log.info('Remote conversation could not be read. Waiting...'); + await Future.delayed(const Duration(seconds: 1)); + retryCount--; + } while (retryCount > 0); + if (remoteConversation == null) { + return AsyncValue.error('Invitation accept timed out.'); + } + + // Complete the local conversation now that we have the remote profile + final remoteProfile = remoteConversation.profile; + final localConversationRecordKey = + contactInvitationRecord.localConversationRecordKey.toVeilid(); + return conversation.initLocalConversation( + existingConversationRecordKey: localConversationRecordKey, + profile: account.profile, + // ignore: prefer_expression_function_bodies + callback: (localConversation) async { + return AsyncValue.data(InvitationStatus( + acceptedContact: AcceptedContact( + remoteProfile: remoteProfile, + remoteIdentity: contactIdentityMaster, + remoteConversationRecordKey: remoteConversationRecordKey, + localConversationRecordKey: localConversationRecordKey))); + }); + } +} + + + // Future checkInvitationStatus( + // {}) async { + // // Open the contact request inbox + // try { + // final pool = DHTRecordPool.instance; + // final accountRecordKey = _activeAccountInfo + // .userLogin.accountRecordInfo.accountRecord.recordKey; + // final writerKey = contactInvitationRecord.writerKey.toVeilid(); + // final writerSecret = contactInvitationRecord.writerSecret.toVeilid(); + // final recordKey = + // contactInvitationRecord.contactRequestInbox.recordKey.toVeilid(); + // final writer = TypedKeyPair( + // kind: recordKey.kind, key: writerKey, secret: writerSecret); + // final acceptReject = await (await pool.openRead(recordKey, + // crypto: await DHTRecordCryptoPrivate.fromTypedKeyPair(writer), + // parent: accountRecordKey, + // defaultSubkey: 1)) + // .scope((contactRequestInbox) async { + // // + // final signedContactResponse = await contactRequestInbox.getProtobuf( + // proto.SignedContactResponse.fromBuffer, + // forceRefresh: true); + // if (signedContactResponse == null) { + // return null; + // } + + // final contactResponseBytes = + // Uint8List.fromList(signedContactResponse.contactResponse); + // final contactResponse = + // proto.ContactResponse.fromBuffer(contactResponseBytes); + // final contactIdentityMasterRecordKey = + // contactResponse.identityMasterRecordKey.toVeilid(); + // final cs = await pool.veilid.getCryptoSystem(recordKey.kind); + + // // Fetch the remote contact's account master + // final contactIdentityMaster = await openIdentityMaster( + // identityMasterRecordKey: contactIdentityMasterRecordKey); + + // // Verify + // final signature = signedContactResponse.identitySignature.toVeilid(); + // await cs.verify(contactIdentityMaster.identityPublicKey, + // contactResponseBytes, signature); + + // // Check for rejection + // if (!contactResponse.accept) { + // return const InvitationStatus(acceptedContact: null); + // } + + // // Pull profile from remote conversation key + // final remoteConversationRecordKey = + // contactResponse.remoteConversationRecordKey.toVeilid(); + + // final conversation = ConversationCubit( + // activeAccountInfo: _activeAccountInfo, + // remoteIdentityPublicKey: + // contactIdentityMaster.identityPublicTypedKey(), + // remoteConversationRecordKey: remoteConversationRecordKey); + // await conversation.refresh(); + + // final remoteConversation = + // conversation.state.data?.value.remoteConversation; + // if (remoteConversation == null) { + // log.info('Remote conversation could not be read. Waiting...'); + // return null; + // } + + // // Complete the local conversation now that we have the remote profile + // final localConversationRecordKey = + // contactInvitationRecord.localConversationRecordKey.toVeilid(); + // return conversation.initLocalConversation( + // existingConversationRecordKey: localConversationRecordKey, + // profile: _account.profile, + // // ignore: prefer_expression_function_bodies + // callback: (localConversation) async { + // return InvitationStatus( + // acceptedContact: AcceptedContact( + // remoteProfile: remoteConversation.profile, + // remoteIdentity: contactIdentityMaster, + // remoteConversationRecordKey: remoteConversationRecordKey, + // localConversationRecordKey: localConversationRecordKey)); + // }); + // }); + + // if (acceptReject == null) { + // return null; + // } + + // // Delete invitation and return the accepted or rejected contact + // await deleteInvitation( + // accepted: acceptReject.acceptedContact != null, + // contactInvitationRecord: contactInvitationRecord); + + // return acceptReject; + // } on Exception catch (e) { + // log.error('Exception in checkInvitationStatus: $e', e); + + // // Attempt to clean up. All this needs better lifetime management + // await deleteInvitation( + // accepted: false, contactInvitationRecord: contactInvitationRecord); + + // rethrow; + // } + + + + + + + diff --git a/lib/contact_invitation/cubits/waiting_invitations_bloc_map_cubit.dart b/lib/contact_invitation/cubits/waiting_invitations_bloc_map_cubit.dart new file mode 100644 index 0000000..812537e --- /dev/null +++ b/lib/contact_invitation/cubits/waiting_invitations_bloc_map_cubit.dart @@ -0,0 +1,58 @@ +import 'package:async_tools/async_tools.dart'; +import 'package:fast_immutable_collections/fast_immutable_collections.dart'; +import 'package:veilid_support/veilid_support.dart'; + +import '../../account_manager/account_manager.dart'; +import '../../proto/proto.dart' as proto; +import '../../tools/tools.dart'; +import 'cubits.dart'; + +typedef WaitingInvitationsBlocMapState + = BlocMapState>; + +// Map of contactInvitationListRecordKey to WaitingInvitationCubit +// Wraps a contact invitation cubit to watch for accept/reject +// Automatically follows the state of a ContactInvitiationListCubit. +class WaitingInvitationsBlocMapCubit extends BlocMapCubit, WaitingInvitationCubit> + with + StateFollower>, + TypedKey, proto.ContactInvitationRecord> { + WaitingInvitationsBlocMapCubit( + {required this.activeAccountInfo, required this.account}); + Future addWaitingInvitation( + {required proto.ContactInvitationRecord + contactInvitationRecord}) async => + add(() => MapEntry( + contactInvitationRecord.contactRequestInbox.recordKey.toVeilid(), + WaitingInvitationCubit( + ContactRequestInboxCubit( + activeAccountInfo: activeAccountInfo, + contactInvitationRecord: contactInvitationRecord), + activeAccountInfo: activeAccountInfo, + account: account, + contactInvitationRecord: contactInvitationRecord))); + + final ActiveAccountInfo activeAccountInfo; + final proto.Account account; + + /// StateFollower ///////////////////////// + @override + IMap getStateMap( + AsyncValue> avstate) { + final state = avstate.data?.value; + if (state == null) { + return IMap(); + } + return IMap.fromIterable(state, + keyMapper: (e) => e.contactRequestInbox.recordKey.toVeilid(), + valueMapper: (e) => e); + } + + @override + Future removeFromState(TypedKey key) => remove(key); + + @override + Future updateState(TypedKey key, proto.ContactInvitationRecord value) => + addWaitingInvitation(contactInvitationRecord: value); +} diff --git a/lib/contact_invitation/models/accepted_contact.dart b/lib/contact_invitation/models/accepted_contact.dart index 4623b60..ac8edc2 100644 --- a/lib/contact_invitation/models/accepted_contact.dart +++ b/lib/contact_invitation/models/accepted_contact.dart @@ -1,10 +1,11 @@ +import 'package:equatable/equatable.dart'; import 'package:meta/meta.dart'; import 'package:veilid_support/veilid_support.dart'; import '../../proto/proto.dart' as proto; @immutable -class AcceptedContact { +class AcceptedContact extends Equatable { const AcceptedContact({ required this.remoteProfile, required this.remoteIdentity, @@ -16,4 +17,12 @@ class AcceptedContact { final IdentityMaster remoteIdentity; final TypedKey remoteConversationRecordKey; final TypedKey localConversationRecordKey; + + @override + List get props => [ + remoteProfile, + remoteIdentity, + remoteConversationRecordKey, + localConversationRecordKey + ]; } diff --git a/lib/contacts/views/contact_item_widget.dart b/lib/contacts/views/contact_item_widget.dart index 8823150..864b9ab 100644 --- a/lib/contacts/views/contact_item_widget.dart +++ b/lib/contacts/views/contact_item_widget.dart @@ -80,20 +80,6 @@ class ContactItemWidget extends StatelessWidget { await MainPager.of(context)?.pageController.animateToPage(1, duration: 250.ms, curve: Curves.easeInOut); } - - // // 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.pronouns.isNotEmpty) @@ -101,7 +87,6 @@ class ContactItemWidget extends StatelessWidget { : null, iconColor: scale.tertiaryScale.background, textColor: scale.tertiaryScale.text, - //Text(Timestamp.fromInt64(contactInvitationRecord.expiration) / ), leading: const Icon(Icons.person)))); } diff --git a/lib/layout/home/home_account_ready/home_account_ready_shell.dart b/lib/layout/home/home_account_ready/home_account_ready_shell.dart index 03fb82e..2434003 100644 --- a/lib/layout/home/home_account_ready/home_account_ready_shell.dart +++ b/lib/layout/home/home_account_ready/home_account_ready_shell.dart @@ -61,11 +61,16 @@ class HomeAccountReadyShellState extends State { activeAccountInfo: activeAccountInfo, account: account)), BlocProvider( - create: (context) => ActiveConversationsCubit( + create: (context) => ActiveConversationsBlocMapCubit( activeAccountInfo: activeAccountInfo)), BlocProvider( - create: (context) => - ActiveChatCubit(null, routerCubit.setHasActiveChat)) + create: (context) => ActiveChatCubit(null) + ..withStateListen((event) { + routerCubit.setHasActiveChat(event != null); + })), + BlocProvider( + create: (context) => WaitingInvitationsBlocMapCubit( + activeAccountInfo: activeAccountInfo, account: account)) ], child: widget.child); }))); } diff --git a/lib/tools/async_transformer_cubit.dart b/lib/tools/async_transformer_cubit.dart new file mode 100644 index 0000000..9ce4bf1 --- /dev/null +++ b/lib/tools/async_transformer_cubit.dart @@ -0,0 +1,62 @@ +import 'dart:async'; + +import 'package:async_tools/async_tools.dart'; +import 'package:bloc/bloc.dart'; + +class AsyncTransformerCubit extends Cubit> { + AsyncTransformerCubit(this.input, {required this.transform}) + : super(const AsyncValue.loading()) { + _asyncTransform(input.state); + _subscription = input.stream.listen(_asyncTransform); + } + void _asyncTransform(AsyncValue newInputState) { + // Use a singlefuture here to ensure we get dont lose any updates + // If the input stream gives us an update while we are + // still processing the last update, the most recent input state will + // be saved and processed eventually. + singleFuture(this, () async { + var newState = newInputState; + var done = false; + while (!done) { + // Emit the transformed state + try { + if (newState is AsyncLoading) { + return AsyncValue.loading(); + } + if (newState is AsyncError) { + final newStateError = newState as AsyncError; + return AsyncValue.error( + newStateError.error, newStateError.stackTrace); + } + final transformedState = await transform(newState.data!.value); + emit(transformedState); + } on Exception catch (e, st) { + emit(AsyncValue.error(e, st)); + } + // See if there's another state change to process + final next = _nextInputState; + _nextInputState = null; + if (next != null) { + newState = next; + } else { + done = true; + } + } + }, onBusy: () { + // Keep this state until we process again + _nextInputState = newInputState; + }); + } + + @override + Future close() async { + await _subscription.cancel(); + await input.close(); + await super.close(); + } + + Cubit> input; + AsyncValue? _nextInputState; + Future> Function(S) transform; + late final StreamSubscription> _subscription; +} diff --git a/lib/tools/bloc_map_cubit.dart b/lib/tools/bloc_map_cubit.dart index 04d7693..2553c66 100644 --- a/lib/tools/bloc_map_cubit.dart +++ b/lib/tools/bloc_map_cubit.dart @@ -12,6 +12,14 @@ class _ItemEntry { final StreamSubscription subscription; } +// Streaming container cubit that is a map from some immutable key +// to a some other cubit's output state. Output state for this container +// cubit is an immutable map of the key to the output state of the contained +// cubits. +// +// K = Key type for the bloc map, used to look up some mapped cubit +// S = State type for the value, keys will look up values of this type +// B = Bloc/cubit type for the value, output states of type S abstract class BlocMapCubit> extends Cubit> { BlocMapCubit() diff --git a/lib/tools/bloc_tools.dart b/lib/tools/bloc_tools.dart new file mode 100644 index 0000000..44940da --- /dev/null +++ b/lib/tools/bloc_tools.dart @@ -0,0 +1,12 @@ +import 'package:bloc/bloc.dart'; + +mixin BlocTools on BlocBase { + void withStateListen(void Function(State event)? onData, + {Function? onError, void Function()? onDone, bool? cancelOnError}) { + if (onData != null) { + onData(state); + } + stream.listen(onData, + onError: onError, onDone: onDone, cancelOnError: cancelOnError); + } +} diff --git a/lib/tools/state_follower.dart b/lib/tools/state_follower.dart new file mode 100644 index 0000000..cf1ab9a --- /dev/null +++ b/lib/tools/state_follower.dart @@ -0,0 +1,78 @@ +import 'dart:async'; + +import 'package:async_tools/async_tools.dart'; +import 'package:fast_immutable_collections/fast_immutable_collections.dart'; + +// Mixin that automatically keeps two blocs/cubits in sync with each other +// Useful for having a BlocMapCubit 'follow' the state of another input cubit. +// As the state of the input cubit changes, the BlocMapCubit can add/remove +// mapped Cubits that automatically process the input state reactively. +// +// S = Input state type +// K = Key derived from elements of input state +// V = Value derived from elements of input state +abstract mixin class StateFollower { + void follow({ + required S initialInputState, + required Stream stream, + }) { + // + _lastInputStateMap = getStateMap(initialInputState); + _subscription = stream.listen(_updateFollow); + } + + Future close() async { + await _subscription.cancel(); + } + + IMap getStateMap(S state); + Future removeFromState(K key); + Future updateState(K key, V value); + + void _updateFollow(S newInputState) { + // Use a singlefuture here to ensure we get dont lose any updates + // If the input stream gives us an update while we are + // still processing the last update, the most recent input state will + // be saved and processed eventually. + final newInputStateMap = getStateMap(newInputState); + + singleFuture(this, () async { + var newStateMap = newInputStateMap; + var done = false; + while (!done) { + for (final k in _lastInputStateMap.keys) { + if (!newStateMap.containsKey(k)) { + // deleted + await removeFromState(k); + } + } + for (final newEntry in newStateMap.entries) { + final v = _lastInputStateMap.get(newEntry.key); + if (v == null || v != newEntry.value) { + // added or changed + await updateState(newEntry.key, newEntry.value); + } + } + + // Keep this state map for the next time + _lastInputStateMap = newStateMap; + + // See if there's another state change to process + final next = _nextInputStateMap; + _nextInputStateMap = null; + if (next != null) { + newStateMap = next; + } else { + done = true; + } + } + }, onBusy: () { + // Keep this state until we process again + _nextInputStateMap = newInputStateMap; + }); + } + + late IMap _lastInputStateMap; + IMap? _nextInputStateMap; + late final StreamSubscription _subscription; +} diff --git a/lib/tools/tools.dart b/lib/tools/tools.dart index 35792b8..4c9cf07 100644 --- a/lib/tools/tools.dart +++ b/lib/tools/tools.dart @@ -1,5 +1,7 @@ export 'animations.dart'; +export 'async_transformer_cubit.dart'; export 'bloc_map_cubit.dart'; +export 'bloc_tools.dart'; export 'enter_password.dart'; export 'enter_pin.dart'; export 'future_cubit.dart'; @@ -8,6 +10,7 @@ export 'phono_byte.dart'; export 'responsive.dart'; export 'scanner_error_widget.dart'; export 'shared_preferences.dart'; +export 'state_follower.dart'; export 'state_logger.dart'; export 'stream_listenable.dart'; export 'stream_wrapper_cubit.dart'; diff --git a/packages/veilid_support/lib/src/memory_tools.dart b/packages/veilid_support/lib/src/memory_tools.dart new file mode 100644 index 0000000..08aa8dc --- /dev/null +++ b/packages/veilid_support/lib/src/memory_tools.dart @@ -0,0 +1,72 @@ +import 'dart:math'; +import 'dart:typed_data'; + +/// Compares two [Uint8List] contents for equality by comparing words at a time. +/// Returns true if this == other +extension Uint8ListCompare on Uint8List { + bool equals(Uint8List other) { + if (identical(this, other)) { + return true; + } + if (length != other.length) { + return false; + } + + final words = buffer.asUint32List(); + final otherwords = other.buffer.asUint32List(); + final wordLen = words.length; + + var i = 0; + for (; i < wordLen; i++) { + if (words[i] != otherwords[i]) { + break; + } + } + i <<= 2; + for (; i < length; i++) { + if (this[i] != other[i]) { + return false; + } + } + return true; + } + + /// Compares two [Uint8List] contents for + /// numeric ordering by comparing words at a time. + /// Returns -1 for this < other, 1 for this > other, and 0 for this == other. + int compare(Uint8List other) { + if (identical(this, other)) { + return 0; + } + + final words = buffer.asUint32List(); + final otherwords = other.buffer.asUint32List(); + final minWordLen = min(words.length, otherwords.length); + + var i = 0; + for (; i < minWordLen; i++) { + if (words[i] != otherwords[i]) { + break; + } + } + i <<= 2; + final minLen = min(length, other.length); + for (; i < minLen; i++) { + final a = this[i]; + final b = other[i]; + if (a < b) { + return -1; + } + if (a > b) { + return 1; + } + } + if (length < other.length) { + return -1; + } + if (length > other.length) { + return 1; + } + return 0; + } +} From c4c7b264aa89689b1b0b594fd9f90cab4c3ff8fd Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Tue, 20 Feb 2024 20:07:35 -0500 Subject: [PATCH 43/68] more state follower --- lib/chat/views/chat_component.dart | 5 +- ..._conversation_messages_bloc_map_cubit.dart | 68 +++++++++++ .../active_conversation_messages_cubit.dart | 113 ------------------ .../active_conversations_bloc_map_cubit.dart | 47 +++++++- lib/chat_list/cubits/cubits.dart | 2 +- .../chat_single_contact_item_widget.dart | 4 - .../waiting_invitations_bloc_map_cubit.dart | 17 +-- .../home_account_ready_shell.dart | 23 +++- 8 files changed, 146 insertions(+), 133 deletions(-) create mode 100644 lib/chat_list/cubits/active_conversation_messages_bloc_map_cubit.dart delete mode 100644 lib/chat_list/cubits/active_conversation_messages_cubit.dart diff --git a/lib/chat/views/chat_component.dart b/lib/chat/views/chat_component.dart index 8e80e15..6e16276 100644 --- a/lib/chat/views/chat_component.dart +++ b/lib/chat/views/chat_component.dart @@ -79,7 +79,8 @@ class ChatComponent extends StatelessWidget { // Get the messages to display // and ensure it is safe to operate() on the MessageCubit for this chat - final avmessages = context.select>?>( (x) => x.state[remoteConversationRecordKey]); if (avmessages == null) { @@ -117,7 +118,7 @@ class ChatComponent extends StatelessWidget { if (message.text.isEmpty) { return; } - await context.read().operate( + await context.read().operate( _remoteConversationRecordKey, closure: (messagesCubit) => messagesCubit.addMessage(message: message)); } diff --git a/lib/chat_list/cubits/active_conversation_messages_bloc_map_cubit.dart b/lib/chat_list/cubits/active_conversation_messages_bloc_map_cubit.dart new file mode 100644 index 0000000..a906bfc --- /dev/null +++ b/lib/chat_list/cubits/active_conversation_messages_bloc_map_cubit.dart @@ -0,0 +1,68 @@ +import 'dart:async'; + +import 'package:async_tools/async_tools.dart'; +import 'package:fast_immutable_collections/fast_immutable_collections.dart'; +import 'package:veilid_support/veilid_support.dart'; + +import '../../account_manager/account_manager.dart'; +import '../../chat/chat.dart'; +import '../../proto/proto.dart' as proto; +import '../../tools/tools.dart'; +import 'active_conversations_bloc_map_cubit.dart'; + +// Map of remoteConversationRecordKey to MessagesCubit +// Wraps a MessagesCubit to stream the latest messages to the state +// Automatically follows the state of a ActiveConversationsBlocMapCubit. +class ActiveConversationMessagesBlocMapCubit extends BlocMapCubit>, MessagesCubit> + with + StateFollower> { + ActiveConversationMessagesBlocMapCubit({ + required ActiveAccountInfo activeAccountInfo, + }) : _activeAccountInfo = activeAccountInfo; + + Future _addConversationMessages( + {required proto.Contact contact, + required proto.Conversation localConversation, + required proto.Conversation remoteConversation}) async => + add(() => MapEntry( + contact.remoteConversationRecordKey.toVeilid(), + MessagesCubit( + activeAccountInfo: _activeAccountInfo, + remoteIdentityPublicKey: contact.identityPublicKey.toVeilid(), + localConversationRecordKey: + contact.localConversationRecordKey.toVeilid(), + remoteConversationRecordKey: + contact.remoteConversationRecordKey.toVeilid(), + localMessagesRecordKey: localConversation.messages.toVeilid(), + remoteMessagesRecordKey: + remoteConversation.messages.toVeilid()))); + + /// StateFollower ///////////////////////// + + @override + IMap> getStateMap( + ActiveConversationsBlocMapState state) => + state; + + @override + Future removeFromState(TypedKey key) => remove(key); + + @override + Future updateState( + TypedKey key, AsyncValue value) async { + await value.when( + data: (state) => _addConversationMessages( + contact: state.contact, + localConversation: state.localConversation, + remoteConversation: state.remoteConversation), + loading: () => addState(key, const AsyncValue.loading()), + error: (error, stackTrace) => + addState(key, AsyncValue.error(error, stackTrace))); + } + + //// + + final ActiveAccountInfo _activeAccountInfo; +} diff --git a/lib/chat_list/cubits/active_conversation_messages_cubit.dart b/lib/chat_list/cubits/active_conversation_messages_cubit.dart deleted file mode 100644 index aad822d..0000000 --- a/lib/chat_list/cubits/active_conversation_messages_cubit.dart +++ /dev/null @@ -1,113 +0,0 @@ -import 'dart:async'; - -import 'package:async_tools/async_tools.dart'; -import 'package:fast_immutable_collections/fast_immutable_collections.dart'; -import 'package:veilid_support/veilid_support.dart'; - -import '../../account_manager/account_manager.dart'; -import '../../chat/chat.dart'; -import '../../proto/proto.dart' as proto; -import '../../tools/tools.dart'; -import 'active_conversations_bloc_map_cubit.dart'; - -class ActiveConversationMessagesCubit extends BlocMapCubit>, MessagesCubit> { - ActiveConversationMessagesCubit({ - required ActiveAccountInfo activeAccountInfo, - required Stream stream, - }) : _activeAccountInfo = activeAccountInfo { - // - _subscription = stream.listen(updateMessageCubits); - } - - @override - Future close() async { - await _subscription.cancel(); - await super.close(); - } - - // Determine which conversations have been added, deleted, or changed - // and update this cubit's state appropriately - void updateMessageCubits(ActiveConversationsBlocMapState newInputState) { - // Use a singlefuture here to ensure we get dont lose any updates - // If the ActiveConversations stream gives us an update while we are - // still processing the last update, the most recent input state will - // be saved and processed eventually. - singleFuture(this, () async { - var newActiveConversationsState = newInputState; - var done = false; - while (!done) { - // Build lists of changes to conversations - final deleted = _lastActiveConversationsState.keys - .where((k) => !newActiveConversationsState.containsKey(k)); - final added = newActiveConversationsState.keys - .where((k) => !_lastActiveConversationsState.containsKey(k)); - final changed = _lastActiveConversationsState.where((k, v) { - final nv = newActiveConversationsState[k]; - if (nv == null) { - return false; - } - return nv != v; - }).keys; - - // Process all deleted conversations - for (final d in deleted) { - await remove(d); - } - - // Process all added and changed conversations - for (final a in [...added, ...changed]) { - final av = newActiveConversationsState[a]!; - await av.when( - data: (state) => _addConversationMessages( - contact: state.contact, - localConversation: state.localConversation, - remoteConversation: state.remoteConversation), - loading: () => addState(a, const AsyncValue.loading()), - error: (error, stackTrace) => - addState(a, AsyncValue.error(error, stackTrace))); - } - - // Keep this state for the next time - _lastActiveConversationsState = newActiveConversationsState; - - // See if there's another state change to process - final next = _nextActiveConversationsState; - _nextActiveConversationsState = null; - if (next != null) { - newActiveConversationsState = next; - } else { - done = true; - } - } - }, onBusy: () { - // Keep this state until we process again - _nextActiveConversationsState = newInputState; - }); - } - - Future _addConversationMessages( - {required proto.Contact contact, - required proto.Conversation localConversation, - required proto.Conversation remoteConversation}) async => - add(() => MapEntry( - contact.remoteConversationRecordKey.toVeilid(), - MessagesCubit( - activeAccountInfo: _activeAccountInfo, - remoteIdentityPublicKey: contact.identityPublicKey.toVeilid(), - localConversationRecordKey: - contact.localConversationRecordKey.toVeilid(), - remoteConversationRecordKey: - contact.remoteConversationRecordKey.toVeilid(), - localMessagesRecordKey: localConversation.messages.toVeilid(), - remoteMessagesRecordKey: - remoteConversation.messages.toVeilid()))); - - //// - - final ActiveAccountInfo _activeAccountInfo; - ActiveConversationsBlocMapState _lastActiveConversationsState = - ActiveConversationsBlocMapState(); - ActiveConversationsBlocMapState? _nextActiveConversationsState; - late final StreamSubscription _subscription; -} diff --git a/lib/chat_list/cubits/active_conversations_bloc_map_cubit.dart b/lib/chat_list/cubits/active_conversations_bloc_map_cubit.dart index 946ec99..78bf8ff 100644 --- a/lib/chat_list/cubits/active_conversations_bloc_map_cubit.dart +++ b/lib/chat_list/cubits/active_conversations_bloc_map_cubit.dart @@ -1,5 +1,6 @@ import 'package:async_tools/async_tools.dart'; import 'package:equatable/equatable.dart'; +import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:meta/meta.dart'; import 'package:veilid_support/veilid_support.dart'; @@ -32,11 +33,15 @@ typedef ActiveConversationsBlocMapState // Map of remoteConversationRecordKey to ActiveConversationCubit // Wraps a conversation cubit to only expose completely built conversations +// Automatically follows the state of a ChatListCubit. class ActiveConversationsBlocMapCubit extends BlocMapCubit, ActiveConversationCubit> { + AsyncValue, ActiveConversationCubit> + with StateFollower>, TypedKey, proto.Chat> { ActiveConversationsBlocMapCubit( - {required ActiveAccountInfo activeAccountInfo}) - : _activeAccountInfo = activeAccountInfo; + {required ActiveAccountInfo activeAccountInfo, + required ContactListCubit contactListCubit}) + : _activeAccountInfo = activeAccountInfo, + _contactListCubit = contactListCubit; // Add an active conversation to be tracked for changes Future addConversation({required proto.Contact contact}) async => @@ -65,5 +70,41 @@ class ActiveConversationsBlocMapCubit extends BlocMapCubit getStateMap(AsyncValue> state) { + final stateValue = state.data?.value; + if (stateValue == null) { + return IMap(); + } + return IMap.fromIterable(stateValue, + keyMapper: (e) => e.remoteConversationKey.toVeilid(), + valueMapper: (e) => e); + } + + @override + Future removeFromState(TypedKey key) => remove(key); + + @override + Future updateState(TypedKey key, proto.Chat value) async { + final contactList = _contactListCubit.state.data?.value; + if (contactList == null) { + await addState(key, const AsyncValue.loading()); + return; + } + final contactIndex = contactList + .indexWhere((c) => c.remoteConversationRecordKey.toVeilid() == key); + if (contactIndex == -1) { + await addState(key, AsyncValue.error('Contact not found for chat')); + return; + } + final contact = contactList[contactIndex]; + await addConversation(contact: contact); + } + + //// + final ActiveAccountInfo _activeAccountInfo; + final ContactListCubit _contactListCubit; } diff --git a/lib/chat_list/cubits/cubits.dart b/lib/chat_list/cubits/cubits.dart index 7ff0db1..0f099ca 100644 --- a/lib/chat_list/cubits/cubits.dart +++ b/lib/chat_list/cubits/cubits.dart @@ -1,3 +1,3 @@ -export 'active_conversation_messages_cubit.dart'; +export 'active_conversation_messages_bloc_map_cubit.dart'; export 'active_conversations_bloc_map_cubit.dart'; export 'chat_list_cubit.dart'; diff --git a/lib/chat_list/views/chat_single_contact_item_widget.dart b/lib/chat_list/views/chat_single_contact_item_widget.dart index 5bb39ee..7d64e43 100644 --- a/lib/chat_list/views/chat_single_contact_item_widget.dart +++ b/lib/chat_list/views/chat_single_contact_item_widget.dart @@ -68,11 +68,7 @@ class ChatSingleContactItemWidget extends StatelessWidget { // component is not dragged. child: ListTile( onTap: () { - final activeConversationsCubit = - context.read(); singleFuture(activeChatCubit, () async { - await activeConversationsCubit.addConversation( - contact: _contact); activeChatCubit.setActiveChat(remoteConversationRecordKey); }); }, diff --git a/lib/contact_invitation/cubits/waiting_invitations_bloc_map_cubit.dart b/lib/contact_invitation/cubits/waiting_invitations_bloc_map_cubit.dart index 812537e..988c23b 100644 --- a/lib/contact_invitation/cubits/waiting_invitations_bloc_map_cubit.dart +++ b/lib/contact_invitation/cubits/waiting_invitations_bloc_map_cubit.dart @@ -12,7 +12,7 @@ typedef WaitingInvitationsBlocMapState // Map of contactInvitationListRecordKey to WaitingInvitationCubit // Wraps a contact invitation cubit to watch for accept/reject -// Automatically follows the state of a ContactInvitiationListCubit. +// Automatically follows the state of a ContactInvitationListCubit. class WaitingInvitationsBlocMapCubit extends BlocMapCubit, WaitingInvitationCubit> with @@ -33,18 +33,15 @@ class WaitingInvitationsBlocMapCubit extends BlocMapCubit getStateMap( - AsyncValue> avstate) { - final state = avstate.data?.value; - if (state == null) { + AsyncValue> state) { + final stateValue = state.data?.value; + if (stateValue == null) { return IMap(); } - return IMap.fromIterable(state, + return IMap.fromIterable(stateValue, keyMapper: (e) => e.contactRequestInbox.recordKey.toVeilid(), valueMapper: (e) => e); } @@ -55,4 +52,8 @@ class WaitingInvitationsBlocMapCubit extends BlocMapCubit updateState(TypedKey key, proto.ContactInvitationRecord value) => addWaitingInvitation(contactInvitationRecord: value); + + //// + final ActiveAccountInfo activeAccountInfo; + final proto.Account account; } diff --git a/lib/layout/home/home_account_ready/home_account_ready_shell.dart b/lib/layout/home/home_account_ready/home_account_ready_shell.dart index 2434003..fceea75 100644 --- a/lib/layout/home/home_account_ready/home_account_ready_shell.dart +++ b/lib/layout/home/home_account_ready/home_account_ready_shell.dart @@ -1,3 +1,5 @@ +import 'package:async_tools/async_tools.dart'; +import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:provider/provider.dart'; @@ -62,7 +64,19 @@ class HomeAccountReadyShellState extends State { account: account)), BlocProvider( create: (context) => ActiveConversationsBlocMapCubit( - activeAccountInfo: activeAccountInfo)), + activeAccountInfo: activeAccountInfo, + contactListCubit: context.read()) + ..follow( + initialInputState: const AsyncValue.loading(), + stream: context.read().stream)), + BlocProvider( + create: (context) => ActiveConversationMessagesBlocMapCubit( + activeAccountInfo: activeAccountInfo, + )..follow( + initialInputState: IMap(), + stream: context + .read() + .stream)), BlocProvider( create: (context) => ActiveChatCubit(null) ..withStateListen((event) { @@ -70,7 +84,12 @@ class HomeAccountReadyShellState extends State { })), BlocProvider( create: (context) => WaitingInvitationsBlocMapCubit( - activeAccountInfo: activeAccountInfo, account: account)) + activeAccountInfo: activeAccountInfo, account: account) + ..follow( + initialInputState: const AsyncValue.loading(), + stream: context + .read() + .stream)) ], child: widget.child); }))); } From e262b0f77714e6f7f72958be81b0ce8ffcc37ecc Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Sat, 24 Feb 2024 22:27:59 -0500 Subject: [PATCH 44/68] more refactor and dhtrecord multiple-open support --- .../cubits/contact_invitation_list_cubit.dart | 35 +- .../waiting_invitations_bloc_map_cubit.dart | 3 +- .../views/contact_invitation_item_widget.dart | 4 +- .../home_account_ready_shell.dart | 232 ++++++--- lib/layout/home/home_shell.dart | 10 +- lib/router/cubit/router_cubit.dart | 7 +- lib/tools/async_transformer_cubit.dart | 44 +- lib/tools/state_follower.dart | 56 +-- packages/async_tools/lib/async_tools.dart | 4 +- .../async_tools/lib/src/serial_future.dart | 57 +++ .../async_tools/lib/src/single_async.dart | 25 - .../async_tools/lib/src/single_future.dart | 42 ++ .../lib/src/single_state_processor.dart | 46 ++ .../lib/dht_support/dht_support.dart | 1 - .../lib/dht_support/src/dht_record.dart | 157 +++--- .../dht_support/src/dht_record_crypto.dart | 12 +- .../lib/dht_support/src/dht_record_cubit.dart | 14 +- .../lib/dht_support/src/dht_record_pool.dart | 446 +++++++++++++----- .../lib/dht_support/src/dht_short_array.dart | 6 +- 19 files changed, 782 insertions(+), 419 deletions(-) create mode 100644 packages/async_tools/lib/src/serial_future.dart delete mode 100644 packages/async_tools/lib/src/single_async.dart create mode 100644 packages/async_tools/lib/src/single_future.dart create mode 100644 packages/async_tools/lib/src/single_state_processor.dart diff --git a/lib/contact_invitation/cubits/contact_invitation_list_cubit.dart b/lib/contact_invitation/cubits/contact_invitation_list_cubit.dart index 429be53..7deacf1 100644 --- a/lib/contact_invitation/cubits/contact_invitation_list_cubit.dart +++ b/lib/contact_invitation/cubits/contact_invitation_list_cubit.dart @@ -147,7 +147,7 @@ class ContactInvitationListCubit Future deleteInvitation( {required bool accepted, - required proto.ContactInvitationRecord contactInvitationRecord}) async { + required TypedKey contactRequestInboxRecordKey}) async { final pool = DHTRecordPool.instance; final accountRecordKey = _activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey; @@ -159,26 +159,25 @@ class ContactInvitationListCubit if (item == null) { throw Exception('Failed to get contact invitation record'); } - if (item.contactRequestInbox.recordKey == - contactInvitationRecord.contactRequestInbox.recordKey) { + if (item.contactRequestInbox.recordKey.toVeilid() == + contactRequestInboxRecordKey) { await shortArray.tryRemoveItem(i); - break; + + await (await pool.openOwned(item.contactRequestInbox.toVeilid(), + 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.openRead(item.localConversationRecordKey.toVeilid(), + parent: accountRecordKey)) + .delete(); + } + return; } } - await (await pool.openOwned( - contactInvitationRecord.contactRequestInbox.toVeilid(), - 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.openRead( - contactInvitationRecord.localConversationRecordKey.toVeilid(), - parent: accountRecordKey)) - .delete(); - } } Future validateInvitation( diff --git a/lib/contact_invitation/cubits/waiting_invitations_bloc_map_cubit.dart b/lib/contact_invitation/cubits/waiting_invitations_bloc_map_cubit.dart index 988c23b..15f0649 100644 --- a/lib/contact_invitation/cubits/waiting_invitations_bloc_map_cubit.dart +++ b/lib/contact_invitation/cubits/waiting_invitations_bloc_map_cubit.dart @@ -10,7 +10,7 @@ import 'cubits.dart'; typedef WaitingInvitationsBlocMapState = BlocMapState>; -// Map of contactInvitationListRecordKey to WaitingInvitationCubit +// Map of contactRequestInboxRecordKey to WaitingInvitationCubit // Wraps a contact invitation cubit to watch for accept/reject // Automatically follows the state of a ContactInvitationListCubit. class WaitingInvitationsBlocMapCubit extends BlocMapCubit { WaitingInvitationsBlocMapCubit( {required this.activeAccountInfo, required this.account}); + Future addWaitingInvitation( {required proto.ContactInvitationRecord contactInvitationRecord}) async => diff --git a/lib/contact_invitation/views/contact_invitation_item_widget.dart b/lib/contact_invitation/views/contact_invitation_item_widget.dart index 9881a93..36ee50a 100644 --- a/lib/contact_invitation/views/contact_invitation_item_widget.dart +++ b/lib/contact_invitation/views/contact_invitation_item_widget.dart @@ -53,7 +53,9 @@ class ContactInvitationItemWidget extends StatelessWidget { context.read(); await contactInvitationListCubit.deleteInvitation( accepted: false, - contactInvitationRecord: contactInvitationRecord); + contactRequestInboxRecordKey: contactInvitationRecord + .contactRequestInbox.recordKey + .toVeilid()); }, backgroundColor: scale.tertiaryScale.background, foregroundColor: scale.tertiaryScale.text, diff --git a/lib/layout/home/home_account_ready/home_account_ready_shell.dart b/lib/layout/home/home_account_ready/home_account_ready_shell.dart index fceea75..73db595 100644 --- a/lib/layout/home/home_account_ready/home_account_ready_shell.dart +++ b/lib/layout/home/home_account_ready/home_account_ready_shell.dart @@ -1,8 +1,10 @@ import 'package:async_tools/async_tools.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:provider/provider.dart'; +import 'package:veilid_support/veilid_support.dart'; import '../../../account_manager/account_manager.dart'; import '../../../chat/chat.dart'; @@ -13,84 +15,168 @@ import '../../../router/router.dart'; import '../../../tools/tools.dart'; class HomeAccountReadyShell extends StatefulWidget { - const HomeAccountReadyShell({required this.child, super.key}); - - @override - HomeAccountReadyShellState createState() => HomeAccountReadyShellState(); - - final Widget child; -} - -class HomeAccountReadyShellState extends State { - // - @override - void initState() { - super.initState(); - } - - @override - Widget build(BuildContext context) { - // These must be valid already before making this widget, - // per the ShellRoute above it + factory HomeAccountReadyShell( + {required BuildContext context, required Widget child, Key? key}) { + // These must exist in order for the account to + // be considered 'ready' for this widget subtree final activeLocalAccount = context.read().state!; final accountInfo = AccountRepository.instance.getAccountInfo(activeLocalAccount); final activeAccountInfo = accountInfo.activeAccountInfo!; final routerCubit = context.read(); - return Provider.value( - value: activeAccountInfo, - child: BlocProvider( - create: (context) => - AccountRecordCubit(record: activeAccountInfo.accountRecord), - child: Builder(builder: (context) { - final account = - context.watch().state.data?.value; - if (account == null) { - return waitingPage(); - } - return MultiBlocProvider(providers: [ - BlocProvider( - create: (context) => ContactInvitationListCubit( - activeAccountInfo: activeAccountInfo, - account: account)), - BlocProvider( - create: (context) => ContactListCubit( - activeAccountInfo: activeAccountInfo, - account: account)), - BlocProvider( - create: (context) => ChatListCubit( - activeAccountInfo: activeAccountInfo, - account: account)), - BlocProvider( - create: (context) => ActiveConversationsBlocMapCubit( - activeAccountInfo: activeAccountInfo, - contactListCubit: context.read()) - ..follow( - initialInputState: const AsyncValue.loading(), - stream: context.read().stream)), - BlocProvider( - create: (context) => ActiveConversationMessagesBlocMapCubit( - activeAccountInfo: activeAccountInfo, - )..follow( - initialInputState: IMap(), - stream: context - .read() - .stream)), - BlocProvider( - create: (context) => ActiveChatCubit(null) - ..withStateListen((event) { - routerCubit.setHasActiveChat(event != null); - })), - BlocProvider( - create: (context) => WaitingInvitationsBlocMapCubit( - activeAccountInfo: activeAccountInfo, account: account) - ..follow( - initialInputState: const AsyncValue.loading(), - stream: context - .read() - .stream)) - ], child: widget.child); - }))); + return HomeAccountReadyShell._( + activeLocalAccount: activeLocalAccount, + accountInfo: accountInfo, + activeAccountInfo: activeAccountInfo, + routerCubit: routerCubit, + key: key, + child: child); + } + const HomeAccountReadyShell._( + {required this.activeLocalAccount, + required this.accountInfo, + required this.activeAccountInfo, + required this.routerCubit, + required this.child, + super.key}); + + @override + HomeAccountReadyShellState createState() => HomeAccountReadyShellState(); + + final Widget child; + final TypedKey activeLocalAccount; + final AccountInfo accountInfo; + final ActiveAccountInfo activeAccountInfo; + final RouterCubit routerCubit; + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(DiagnosticsProperty( + 'activeLocalAccount', activeLocalAccount)) + ..add(DiagnosticsProperty('accountInfo', accountInfo)) + ..add(DiagnosticsProperty( + 'activeAccountInfo', activeAccountInfo)) + ..add(DiagnosticsProperty('routerCubit', routerCubit)); } } + +class HomeAccountReadyShellState extends State { + final SingleStateProcessor + _singleInvitationStatusProcessor = SingleStateProcessor(); + + @override + void initState() { + super.initState(); + } + + // Process all accepted or rejected invitations + void _invitationStatusListener( + BuildContext context, WaitingInvitationsBlocMapState state) { + _singleInvitationStatusProcessor.updateState(state, + closure: (newState) async { + final contactListCubit = context.read(); + final contactInvitationListCubit = + context.read(); + + for (final entry in newState.entries) { + final contactRequestInboxRecordKey = entry.key; + final invStatus = entry.value.data?.value; + // Skip invitations that have not yet been accepted or rejected + if (invStatus == null) { + continue; + } + + // Delete invitation and process the accepted or rejected contact + final acceptedContact = invStatus.acceptedContact; + if (acceptedContact != null) { + await contactInvitationListCubit.deleteInvitation( + accepted: true, + contactRequestInboxRecordKey: contactRequestInboxRecordKey); + + // Accept + await contactListCubit.createContact( + remoteProfile: acceptedContact.remoteProfile, + remoteIdentity: acceptedContact.remoteIdentity, + remoteConversationRecordKey: + acceptedContact.remoteConversationRecordKey, + localConversationRecordKey: + acceptedContact.localConversationRecordKey, + ); + } else { + // Reject + await contactInvitationListCubit.deleteInvitation( + accepted: false, + contactRequestInboxRecordKey: contactRequestInboxRecordKey); + } + } + }); + } + + @override + Widget build(BuildContext context) => Provider.value( + value: widget.activeAccountInfo, + child: BlocProvider( + create: (context) => AccountRecordCubit( + record: widget.activeAccountInfo.accountRecord), + child: Builder(builder: (context) { + final account = + context.watch().state.data?.value; + if (account == null) { + return waitingPage(); + } + return MultiBlocProvider( + providers: [ + BlocProvider( + create: (context) => ContactInvitationListCubit( + activeAccountInfo: widget.activeAccountInfo, + account: account)), + BlocProvider( + create: (context) => ContactListCubit( + activeAccountInfo: widget.activeAccountInfo, + account: account)), + BlocProvider( + create: (context) => ChatListCubit( + activeAccountInfo: widget.activeAccountInfo, + account: account)), + BlocProvider( + create: (context) => ActiveConversationsBlocMapCubit( + activeAccountInfo: widget.activeAccountInfo, + contactListCubit: context.read()) + ..follow( + initialInputState: const AsyncValue.loading(), + stream: context.read().stream)), + BlocProvider( + create: (context) => + ActiveConversationMessagesBlocMapCubit( + activeAccountInfo: widget.activeAccountInfo, + )..follow( + initialInputState: IMap(), + stream: context + .read() + .stream)), + BlocProvider( + create: (context) => ActiveChatCubit(null) + ..withStateListen((event) { + widget.routerCubit.setHasActiveChat(event != null); + })), + BlocProvider( + create: (context) => WaitingInvitationsBlocMapCubit( + activeAccountInfo: widget.activeAccountInfo, + account: account) + ..follow( + initialInputState: const AsyncValue.loading(), + stream: context + .read() + .stream)) + ], + child: MultiBlocListener(listeners: [ + BlocListener( + listener: _invitationStatusListener, + ) + ], child: widget.child)); + }))); +} diff --git a/lib/layout/home/home_shell.dart b/lib/layout/home/home_shell.dart index 86969fc..839a8a3 100644 --- a/lib/layout/home/home_shell.dart +++ b/lib/layout/home/home_shell.dart @@ -10,12 +10,12 @@ import 'home_account_missing.dart'; import 'home_no_active.dart'; class HomeShell extends StatefulWidget { - const HomeShell({required this.child, super.key}); + const HomeShell({required this.accountReadyBuilder, super.key}); @override HomeShellState createState() => HomeShellState(); - final Widget child; + final Builder accountReadyBuilder; } class HomeShellState extends State { @@ -32,7 +32,7 @@ class HomeShellState extends State { super.dispose(); } - Widget buildWithLogin(BuildContext context, Widget child) { + Widget buildWithLogin(BuildContext context) { final activeLocalAccount = context.watch().state; if (activeLocalAccount == null) { @@ -56,7 +56,7 @@ class HomeShellState extends State { child: BlocProvider( create: (context) => AccountRecordCubit( record: accountInfo.activeAccountInfo!.accountRecord), - child: child)); + child: widget.accountReadyBuilder)); } } @@ -72,6 +72,6 @@ class HomeShellState extends State { child: DecoratedBox( decoration: BoxDecoration( color: scale.primaryScale.activeElementBackground), - child: buildWithLogin(context, widget.child)))); + child: buildWithLogin(context)))); } } diff --git a/lib/router/cubit/router_cubit.dart b/lib/router/cubit/router_cubit.dart index 88d7537..296e448 100644 --- a/lib/router/cubit/router_cubit.dart +++ b/lib/router/cubit/router_cubit.dart @@ -3,6 +3,7 @@ import 'dart:async'; import 'package:bloc/bloc.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:go_router/go_router.dart'; import 'package:stream_transform/stream_transform.dart'; @@ -68,8 +69,10 @@ class RouterCubit extends Cubit { ), ShellRoute( navigatorKey: _homeNavKey, - builder: (context, state, child) => - HomeShell(child: HomeAccountReadyShell(child: child)), + builder: (context, state, child) => HomeShell( + accountReadyBuilder: Builder( + builder: (context) => + HomeAccountReadyShell(context: context, child: child))), routes: [ GoRoute( path: '/home', diff --git a/lib/tools/async_transformer_cubit.dart b/lib/tools/async_transformer_cubit.dart index 9ce4bf1..83691c6 100644 --- a/lib/tools/async_transformer_cubit.dart +++ b/lib/tools/async_transformer_cubit.dart @@ -10,41 +10,20 @@ class AsyncTransformerCubit extends Cubit> { _subscription = input.stream.listen(_asyncTransform); } void _asyncTransform(AsyncValue newInputState) { - // Use a singlefuture here to ensure we get dont lose any updates - // If the input stream gives us an update while we are - // still processing the last update, the most recent input state will - // be saved and processed eventually. - singleFuture(this, () async { - var newState = newInputState; - var done = false; - while (!done) { - // Emit the transformed state - try { - if (newState is AsyncLoading) { - return AsyncValue.loading(); - } - if (newState is AsyncError) { - final newStateError = newState as AsyncError; - return AsyncValue.error( - newStateError.error, newStateError.stackTrace); - } + _singleStateProcessor.updateState(newInputState, closure: (newState) async { + // Emit the transformed state + try { + if (newState is AsyncLoading) { + emit(const AsyncValue.loading()); + } else if (newState is AsyncError) { + emit(AsyncValue.error(newState.error, newState.stackTrace)); + } else { final transformedState = await transform(newState.data!.value); emit(transformedState); - } on Exception catch (e, st) { - emit(AsyncValue.error(e, st)); - } - // See if there's another state change to process - final next = _nextInputState; - _nextInputState = null; - if (next != null) { - newState = next; - } else { - done = true; } + } on Exception catch (e, st) { + emit(AsyncValue.error(e, st)); } - }, onBusy: () { - // Keep this state until we process again - _nextInputState = newInputState; }); } @@ -56,7 +35,8 @@ class AsyncTransformerCubit extends Cubit> { } Cubit> input; - AsyncValue? _nextInputState; + final SingleStateProcessor> _singleStateProcessor = + SingleStateProcessor(); Future> Function(S) transform; late final StreamSubscription> _subscription; } diff --git a/lib/tools/state_follower.dart b/lib/tools/state_follower.dart index cf1ab9a..04b8138 100644 --- a/lib/tools/state_follower.dart +++ b/lib/tools/state_follower.dart @@ -30,49 +30,29 @@ abstract mixin class StateFollower { Future updateState(K key, V value); void _updateFollow(S newInputState) { - // Use a singlefuture here to ensure we get dont lose any updates - // If the input stream gives us an update while we are - // still processing the last update, the most recent input state will - // be saved and processed eventually. - final newInputStateMap = getStateMap(newInputState); - - singleFuture(this, () async { - var newStateMap = newInputStateMap; - var done = false; - while (!done) { - for (final k in _lastInputStateMap.keys) { - if (!newStateMap.containsKey(k)) { - // deleted - await removeFromState(k); - } - } - for (final newEntry in newStateMap.entries) { - final v = _lastInputStateMap.get(newEntry.key); - if (v == null || v != newEntry.value) { - // added or changed - await updateState(newEntry.key, newEntry.value); - } - } - - // Keep this state map for the next time - _lastInputStateMap = newStateMap; - - // See if there's another state change to process - final next = _nextInputStateMap; - _nextInputStateMap = null; - if (next != null) { - newStateMap = next; - } else { - done = true; + _singleStateProcessor.updateState(getStateMap(newInputState), + closure: (newStateMap) async { + for (final k in _lastInputStateMap.keys) { + if (!newStateMap.containsKey(k)) { + // deleted + await removeFromState(k); } } - }, onBusy: () { - // Keep this state until we process again - _nextInputStateMap = newInputStateMap; + for (final newEntry in newStateMap.entries) { + final v = _lastInputStateMap.get(newEntry.key); + if (v == null || v != newEntry.value) { + // added or changed + await updateState(newEntry.key, newEntry.value); + } + } + + // Keep this state map for the next time + _lastInputStateMap = newStateMap; }); } late IMap _lastInputStateMap; - IMap? _nextInputStateMap; + final SingleStateProcessor> _singleStateProcessor = + SingleStateProcessor(); late final StreamSubscription _subscription; } diff --git a/packages/async_tools/lib/async_tools.dart b/packages/async_tools/lib/async_tools.dart index 4dbe72e..61d7e7b 100644 --- a/packages/async_tools/lib/async_tools.dart +++ b/packages/async_tools/lib/async_tools.dart @@ -3,4 +3,6 @@ library; export 'src/async_tag_lock.dart'; export 'src/async_value.dart'; -export 'src/single_async.dart'; +export 'src/serial_future.dart'; +export 'src/single_future.dart'; +export 'src/single_state_processor.dart'; diff --git a/packages/async_tools/lib/src/serial_future.dart b/packages/async_tools/lib/src/serial_future.dart new file mode 100644 index 0000000..17225b7 --- /dev/null +++ b/packages/async_tools/lib/src/serial_future.dart @@ -0,0 +1,57 @@ +// Process a single future at a time per tag queued serially +// +// The closure function is called to produce the future that is to be executed. +// If a future with a particular tag is still executing, it is queued serially +// and executed when the previous tagged future completes. +// When a tagged serialFuture finishes executing, the onDone callback is called. +// If an unhandled exception happens in the closure future, the onError callback +// is called. + +import 'dart:async'; +import 'dart:collection'; + +import 'async_tag_lock.dart'; + +AsyncTagLock _keys = AsyncTagLock(); +typedef SerialFutureQueueItem = Future Function(); +Map> _queues = {}; + +SerialFutureQueueItem _makeSerialFutureQueueItem( + Future Function() closure, + void Function(T)? onDone, + void Function(Object e, StackTrace? st)? onError) => + () async { + try { + final out = await closure(); + if (onDone != null) { + onDone(out); + } + // ignore: avoid_catches_without_on_clauses + } catch (e, sp) { + if (onError != null) { + onError(e, sp); + } else { + rethrow; + } + } + }; + +void serialFuture(Object tag, Future Function() closure, + {void Function(T)? onDone, + void Function(Object e, StackTrace? st)? onError}) { + final queueItem = _makeSerialFutureQueueItem(closure, onDone, onError); + if (!_keys.tryLock(tag)) { + final queue = _queues[tag]; + queue!.add(queueItem); + return; + } + final queue = _queues[tag] = Queue.from([queueItem]); + unawaited(() async { + do { + final queueItem = queue.removeFirst(); + await queueItem(); + } while (queue.isNotEmpty); + _queues.remove(tag); + _keys.unlockTag(tag); + }()); +} diff --git a/packages/async_tools/lib/src/single_async.dart b/packages/async_tools/lib/src/single_async.dart deleted file mode 100644 index 82334d7..0000000 --- a/packages/async_tools/lib/src/single_async.dart +++ /dev/null @@ -1,25 +0,0 @@ -import 'dart:async'; - -import 'async_tag_lock.dart'; - -AsyncTagLock _keys = AsyncTagLock(); - -void singleFuture(Object tag, Future Function() closure, - {void Function()? onBusy, void Function(T)? onDone}) { - if (!_keys.tryLock(tag)) { - if (onBusy != null) { - onBusy(); - } - return; - } - unawaited(() async { - try { - final out = await closure(); - if (onDone != null) { - onDone(out); - } - } finally { - _keys.unlockTag(tag); - } - }()); -} diff --git a/packages/async_tools/lib/src/single_future.dart b/packages/async_tools/lib/src/single_future.dart new file mode 100644 index 0000000..7e82e7c --- /dev/null +++ b/packages/async_tools/lib/src/single_future.dart @@ -0,0 +1,42 @@ +import 'dart:async'; + +import 'async_tag_lock.dart'; + +AsyncTagLock _keys = AsyncTagLock(); + +// Process a single future at a time per tag +// +// The closure function is called to produce the future that is to be executed. +// If a future with a particular tag is still executing, the onBusy callback +// is called. +// When a tagged singleFuture finishes executing, the onDone callback is called. +// If an unhandled exception happens in the closure future, the onError callback +// is called. +void singleFuture(Object tag, Future Function() closure, + {void Function()? onBusy, + void Function(T)? onDone, + void Function(Object e, StackTrace? st)? onError}) { + if (!_keys.tryLock(tag)) { + if (onBusy != null) { + onBusy(); + } + return; + } + unawaited(() async { + try { + final out = await closure(); + if (onDone != null) { + onDone(out); + } + // ignore: avoid_catches_without_on_clauses + } catch (e, sp) { + if (onError != null) { + onError(e, sp); + } else { + rethrow; + } + } finally { + _keys.unlockTag(tag); + } + }()); +} diff --git a/packages/async_tools/lib/src/single_state_processor.dart b/packages/async_tools/lib/src/single_state_processor.dart new file mode 100644 index 0000000..ea1af10 --- /dev/null +++ b/packages/async_tools/lib/src/single_state_processor.dart @@ -0,0 +1,46 @@ +import 'dart:async'; + +import '../async_tools.dart'; + +// Process a single state update at a time ensuring the most +// recent state gets processed asynchronously, possibly skipping +// states that happen while a previous state is still being processed. +// +// Eventually this will always process the most recent state passed to +// updateState. +// +// This is useful for processing state changes asynchronously without waiting +// from a synchronous execution context +class SingleStateProcessor { + SingleStateProcessor(); + + void updateState(State newInputState, + {required Future Function(State) closure}) { + // Use a singlefuture here to ensure we get dont lose any updates + // If the input stream gives us an update while we are + // still processing the last update, the most recent input state will + // be saved and processed eventually. + + singleFuture(this, () async { + var newState = newInputState; + var done = false; + while (!done) { + await closure(newState); + + // See if there's another state change to process + final next = _nextState; + _nextState = null; + if (next != null) { + newState = next; + } else { + done = true; + } + } + }, onBusy: () { + // Keep this state until we process again + _nextState = newInputState; + }); + } + + State? _nextState; +} diff --git a/packages/veilid_support/lib/dht_support/dht_support.dart b/packages/veilid_support/lib/dht_support/dht_support.dart index 5ac4022..24f584f 100644 --- a/packages/veilid_support/lib/dht_support/dht_support.dart +++ b/packages/veilid_support/lib/dht_support/dht_support.dart @@ -2,7 +2,6 @@ library dht_support; -export 'src/dht_record.dart'; export 'src/dht_record_crypto.dart'; export 'src/dht_record_cubit.dart'; export 'src/dht_record_pool.dart'; diff --git a/packages/veilid_support/lib/dht_support/src/dht_record.dart b/packages/veilid_support/lib/dht_support/src/dht_record.dart index d9a1337..a94aa3a 100644 --- a/packages/veilid_support/lib/dht_support/src/dht_record.dart +++ b/packages/veilid_support/lib/dht_support/src/dht_record.dart @@ -1,12 +1,4 @@ -import 'dart:async'; -import 'dart:typed_data'; - -import 'package:equatable/equatable.dart'; -import 'package:fast_immutable_collections/fast_immutable_collections.dart'; -import 'package:meta/meta.dart'; -import 'package:protobuf/protobuf.dart'; - -import '../../../veilid_support.dart'; +part of 'dht_record_pool.dart'; @immutable class DHTRecordWatchChange extends Equatable { @@ -14,7 +6,7 @@ class DHTRecordWatchChange extends Equatable { {required this.local, required this.data, required this.subkeys}); final bool local; - final Uint8List data; + final Uint8List? data; final List subkeys; @override @@ -26,46 +18,41 @@ class DHTRecordWatchChange extends Equatable { class DHTRecord { DHTRecord( {required VeilidRoutingContext routingContext, - required DHTRecordDescriptor recordDescriptor, - int defaultSubkey = 0, - KeyPair? writer, - DHTRecordCrypto crypto = const DHTRecordCryptoPublic()}) + required SharedDHTRecordData sharedDHTRecordData, + required int defaultSubkey, + required KeyPair? writer, + required DHTRecordCrypto crypto}) : _crypto = crypto, _routingContext = routingContext, - _recordDescriptor = recordDescriptor, _defaultSubkey = defaultSubkey, _writer = writer, _open = true, _valid = true, - _subkeySeqCache = {}, - needsWatchStateUpdate = false, - inWatchStateUpdate = false; + _sharedDHTRecordData = sharedDHTRecordData; + final SharedDHTRecordData _sharedDHTRecordData; final VeilidRoutingContext _routingContext; - final DHTRecordDescriptor _recordDescriptor; final int _defaultSubkey; final KeyPair? _writer; - final Map _subkeySeqCache; final DHTRecordCrypto _crypto; + bool _open; bool _valid; @internal StreamController? watchController; @internal - bool needsWatchStateUpdate; - @internal - bool inWatchStateUpdate; - @internal WatchState? watchState; int subkeyOrDefault(int subkey) => (subkey == -1) ? _defaultSubkey : subkey; VeilidRoutingContext get routingContext => _routingContext; - TypedKey get key => _recordDescriptor.key; - PublicKey get owner => _recordDescriptor.owner; - KeyPair? get ownerKeyPair => _recordDescriptor.ownerKeyPair(); - DHTSchema get schema => _recordDescriptor.schema; - int get subkeyCount => _recordDescriptor.schema.subkeyCount(); + TypedKey get key => _sharedDHTRecordData.recordDescriptor.key; + PublicKey get owner => _sharedDHTRecordData.recordDescriptor.owner; + KeyPair? get ownerKeyPair => + _sharedDHTRecordData.recordDescriptor.ownerKeyPair(); + DHTSchema get schema => _sharedDHTRecordData.recordDescriptor.schema; + int get subkeyCount => + _sharedDHTRecordData.recordDescriptor.schema.subkeyCount(); KeyPair? get writer => _writer; DHTRecordCrypto get crypto => _crypto; OwnedDHTRecordPointer get ownedDHTRecordPointer => @@ -79,22 +66,16 @@ class DHTRecord { return; } await watchController?.close(); - await _routingContext.closeDHTRecord(_recordDescriptor.key); - DHTRecordPool.instance.recordClosed(_recordDescriptor.key); + await DHTRecordPool.instance._recordClosed(this); _open = false; } - Future delete() async { - if (!_valid) { - throw StateError('already deleted'); - } - if (_open) { - await close(); - } - await DHTRecordPool.instance.deleteDeep(key); + void _markDeleted() { _valid = false; } + Future delete() => DHTRecordPool.instance.delete(key); + Future scope(Future Function(DHTRecord) scopeFunction) async { try { return await scopeFunction(this); @@ -134,17 +115,17 @@ class DHTRecord { bool forceRefresh = false, bool onlyUpdates = false}) async { subkey = subkeyOrDefault(subkey); - final valueData = await _routingContext.getDHTValue( - _recordDescriptor.key, subkey, forceRefresh); + final valueData = await _routingContext.getDHTValue(key, subkey, + forceRefresh: forceRefresh); if (valueData == null) { return null; } - final lastSeq = _subkeySeqCache[subkey]; + final lastSeq = _sharedDHTRecordData.subkeySeqCache[subkey]; if (onlyUpdates && lastSeq != null && valueData.seq <= lastSeq) { return null; } final out = _crypto.decrypt(valueData.data, subkey); - _subkeySeqCache[subkey] = valueData.seq; + _sharedDHTRecordData.subkeySeqCache[subkey] = valueData.seq; return out; } @@ -176,17 +157,16 @@ class DHTRecord { Future tryWriteBytes(Uint8List newValue, {int subkey = -1}) async { subkey = subkeyOrDefault(subkey); - final lastSeq = _subkeySeqCache[subkey]; + final lastSeq = _sharedDHTRecordData.subkeySeqCache[subkey]; final encryptedNewValue = await _crypto.encrypt(newValue, subkey); // Set the new data if possible - var newValueData = await _routingContext.setDHTValue( - _recordDescriptor.key, subkey, encryptedNewValue); + var newValueData = + await _routingContext.setDHTValue(key, subkey, encryptedNewValue); if (newValueData == null) { // A newer value wasn't found on the set, but // we may get a newer value when getting the value for the sequence number - newValueData = await _routingContext.getDHTValue( - _recordDescriptor.key, subkey, false); + newValueData = await _routingContext.getDHTValue(key, subkey); if (newValueData == null) { assert(newValueData != null, "can't get value that was just set"); return null; @@ -195,13 +175,13 @@ class DHTRecord { // Record new sequence number final isUpdated = newValueData.seq != lastSeq; - _subkeySeqCache[subkey] = newValueData.seq; + _sharedDHTRecordData.subkeySeqCache[subkey] = newValueData.seq; // See if the encrypted data returned is exactly the same // if so, shortcut and don't bother decrypting it if (newValueData.data.equals(encryptedNewValue)) { if (isUpdated) { - addLocalValueChange(newValue, subkey); + _addLocalValueChange(newValue, subkey); } return null; } @@ -209,36 +189,35 @@ class DHTRecord { // Decrypt value to return it final decryptedNewValue = await _crypto.decrypt(newValueData.data, subkey); if (isUpdated) { - addLocalValueChange(decryptedNewValue, subkey); + _addLocalValueChange(decryptedNewValue, subkey); } return decryptedNewValue; } Future eventualWriteBytes(Uint8List newValue, {int subkey = -1}) async { subkey = subkeyOrDefault(subkey); - final lastSeq = _subkeySeqCache[subkey]; + final lastSeq = _sharedDHTRecordData.subkeySeqCache[subkey]; final encryptedNewValue = await _crypto.encrypt(newValue, subkey); ValueData? newValueData; do { do { // Set the new data - newValueData = await _routingContext.setDHTValue( - _recordDescriptor.key, subkey, encryptedNewValue); + newValueData = + await _routingContext.setDHTValue(key, subkey, encryptedNewValue); // Repeat if newer data on the network was found } while (newValueData != null); // Get the data to check its sequence number - newValueData = await _routingContext.getDHTValue( - _recordDescriptor.key, subkey, false); + newValueData = await _routingContext.getDHTValue(key, subkey); if (newValueData == null) { assert(newValueData != null, "can't get value that was just set"); return; } // Record new sequence number - _subkeySeqCache[subkey] = newValueData.seq; + _sharedDHTRecordData.subkeySeqCache[subkey] = newValueData.seq; // The encrypted data returned should be exactly the same // as what we are trying to set, @@ -247,7 +226,7 @@ class DHTRecord { final isUpdated = newValueData.seq != lastSeq; if (isUpdated) { - addLocalValueChange(newValue, subkey); + _addLocalValueChange(newValue, subkey); } } @@ -258,8 +237,7 @@ class DHTRecord { // Get the existing data, do not allow force refresh here // because if we need a refresh the setDHTValue will fail anyway - var oldValue = - await get(subkey: subkey, forceRefresh: false, onlyUpdates: false); + var oldValue = await get(subkey: subkey); do { // Update the data @@ -314,16 +292,16 @@ class DHTRecord { int? count}) async { // Set up watch requirements which will get picked up by the next tick final oldWatchState = watchState; - watchState = WatchState( - subkeys: subkeys?.lock, expiration: expiration, count: count); + watchState = + WatchState(subkeys: subkeys, expiration: expiration, count: count); if (oldWatchState != watchState) { - needsWatchStateUpdate = true; + _sharedDHTRecordData.needsWatchStateUpdate = true; } } Future> listen( Future Function( - DHTRecord record, Uint8List data, List subkeys) + DHTRecord record, Uint8List? data, List subkeys) onUpdate, {bool localChanges = true}) async { // Set up watch requirements @@ -339,14 +317,16 @@ class DHTRecord { return; } Future.delayed(Duration.zero, () async { - final Uint8List data; + final Uint8List? data; if (change.local) { // local changes are not encrypted data = change.data; } else { // incoming/remote changes are encrypted - data = - await _crypto.decrypt(change.data, change.subkeys.first.low); + final changeData = change.data; + data = changeData == null + ? null + : await _crypto.decrypt(changeData, change.subkeys.first.low); } await onUpdate(this, data, change.subkeys); }); @@ -362,17 +342,48 @@ class DHTRecord { // Tear down watch requirements if (watchState != null) { watchState = null; - needsWatchStateUpdate = true; + _sharedDHTRecordData.needsWatchStateUpdate = true; } } - void addLocalValueChange(Uint8List data, int subkey) { - watchController?.add(DHTRecordWatchChange( - local: true, data: data, subkeys: [ValueSubkeyRange.single(subkey)])); + void _addValueChange( + {required bool local, + required Uint8List data, + required List subkeys}) { + final ws = watchState; + if (ws != null) { + final watchedSubkeys = ws.subkeys; + if (watchedSubkeys == null) { + // Report all subkeys + watchController?.add( + DHTRecordWatchChange(local: false, data: data, subkeys: subkeys)); + } else { + // Only some subkeys are being watched, see if the reported update + // overlaps the subkeys being watched + final overlappedSubkeys = watchedSubkeys.intersectSubkeys(subkeys); + // If the reported data isn't within the + // range we care about, don't pass it through + final overlappedFirstSubkey = overlappedSubkeys.firstSubkey; + final updateFirstSubkey = subkeys.firstSubkey; + final updatedData = (overlappedFirstSubkey != null && + updateFirstSubkey != null && + overlappedFirstSubkey == updateFirstSubkey) + ? data + : null; + // Report only wathced subkeys + watchController?.add(DHTRecordWatchChange( + local: local, data: updatedData, subkeys: overlappedSubkeys)); + } + } + } + + void _addLocalValueChange(Uint8List data, int subkey) { + _addValueChange( + local: true, data: data, subkeys: [ValueSubkeyRange.single(subkey)]); } void addRemoteValueChange(VeilidUpdateValueChange update) { - watchController?.add(DHTRecordWatchChange( - local: false, data: update.valueData.data, subkeys: update.subkeys)); + _addValueChange( + local: false, data: update.valueData.data, subkeys: update.subkeys); } } diff --git a/packages/veilid_support/lib/dht_support/src/dht_record_crypto.dart b/packages/veilid_support/lib/dht_support/src/dht_record_crypto.dart index 7d59453..60534b6 100644 --- a/packages/veilid_support/lib/dht_support/src/dht_record_crypto.dart +++ b/packages/veilid_support/lib/dht_support/src/dht_record_crypto.dart @@ -3,8 +3,8 @@ import 'dart:typed_data'; import '../../../../veilid_support.dart'; abstract class DHTRecordCrypto { - FutureOr encrypt(Uint8List data, int subkey); - FutureOr decrypt(Uint8List data, int subkey); + Future encrypt(Uint8List data, int subkey); + Future decrypt(Uint8List data, int subkey); } //////////////////////////////////// @@ -32,11 +32,11 @@ class DHTRecordCryptoPrivate implements DHTRecordCrypto { } @override - FutureOr encrypt(Uint8List data, int subkey) => + Future encrypt(Uint8List data, int subkey) => _cryptoSystem.encryptNoAuthWithNonce(data, _secretKey); @override - FutureOr decrypt(Uint8List data, int subkey) => + Future decrypt(Uint8List data, int subkey) => _cryptoSystem.decryptNoAuthWithNonce(data, _secretKey); } @@ -46,8 +46,8 @@ class DHTRecordCryptoPublic implements DHTRecordCrypto { const DHTRecordCryptoPublic(); @override - FutureOr encrypt(Uint8List data, int subkey) => data; + Future encrypt(Uint8List data, int subkey) async => data; @override - FutureOr decrypt(Uint8List data, int subkey) => data; + Future decrypt(Uint8List data, int subkey) async => data; } diff --git a/packages/veilid_support/lib/dht_support/src/dht_record_cubit.dart b/packages/veilid_support/lib/dht_support/src/dht_record_cubit.dart index 9c809b2..4a8eba0 100644 --- a/packages/veilid_support/lib/dht_support/src/dht_record_cubit.dart +++ b/packages/veilid_support/lib/dht_support/src/dht_record_cubit.dart @@ -8,7 +8,7 @@ import '../../veilid_support.dart'; typedef InitialStateFunction = Future Function(DHTRecord); typedef StateFunction = Future Function( - DHTRecord, List, Uint8List); + DHTRecord, List, Uint8List?); class DHTRecordCubit extends Cubit> { DHTRecordCubit({ @@ -28,9 +28,8 @@ class DHTRecordCubit extends Cubit> { DHTRecordCubit.value({ required DHTRecord record, - required Future Function(DHTRecord) initialStateFunction, - required Future Function(DHTRecord, List, Uint8List) - stateFunction, + required InitialStateFunction initialStateFunction, + required StateFunction stateFunction, }) : _record = record, _stateFunction = stateFunction, _wantsCloseRecord = false, @@ -41,9 +40,8 @@ class DHTRecordCubit extends Cubit> { } Future _init( - Future Function(DHTRecord) initialStateFunction, - Future Function(DHTRecord, List, Uint8List) - stateFunction, + InitialStateFunction initialStateFunction, + StateFunction stateFunction, ) async { // Make initial state update try { @@ -142,7 +140,7 @@ class DefaultDHTRecordCubit extends DHTRecordCubit { if (subkeys.containsSubkey(defaultSubkey)) { final Uint8List data; final firstSubkey = subkeys.firstOrNull!.low; - if (firstSubkey != defaultSubkey) { + if (firstSubkey != defaultSubkey || updatedata == null) { final maybeData = await record.get(forceRefresh: true); if (maybeData == null) { return null; diff --git a/packages/veilid_support/lib/dht_support/src/dht_record_pool.dart b/packages/veilid_support/lib/dht_support/src/dht_record_pool.dart index 7c85d96..736329e 100644 --- a/packages/veilid_support/lib/dht_support/src/dht_record_pool.dart +++ b/packages/veilid_support/lib/dht_support/src/dht_record_pool.dart @@ -1,15 +1,21 @@ import 'dart:async'; +import 'dart:math'; +import 'dart:typed_data'; import 'package:async_tools/async_tools.dart'; import 'package:equatable/equatable.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:mutex/mutex.dart'; +import 'package:protobuf/protobuf.dart'; import '../../../../veilid_support.dart'; part 'dht_record_pool.freezed.dart'; part 'dht_record_pool.g.dart'; +part 'dht_record.dart'; + /// Record pool that managed DHTRecords and allows for tagged deletion @freezed class DHTRecordPoolAllocations with _$DHTRecordPoolAllocations { @@ -39,13 +45,14 @@ class OwnedDHTRecordPointer with _$OwnedDHTRecordPointer { } /// Watch state +@immutable class WatchState extends Equatable { const WatchState( {required this.subkeys, required this.expiration, required this.count, this.realExpiration}); - final IList? subkeys; + final List? subkeys; final Timestamp? expiration; final int? count; final Timestamp? realExpiration; @@ -54,23 +61,51 @@ class WatchState extends Equatable { List get props => [subkeys, expiration, count, realExpiration]; } +/// Data shared amongst all DHTRecord instances +class SharedDHTRecordData { + SharedDHTRecordData( + {required this.recordDescriptor, + required this.defaultWriter, + required this.defaultRoutingContext}); + DHTRecordDescriptor recordDescriptor; + KeyPair? defaultWriter; + VeilidRoutingContext defaultRoutingContext; + Map subkeySeqCache = {}; + bool inWatchStateUpdate = false; + bool needsWatchStateUpdate = false; +} + +// Per opened record data +class OpenedRecordInfo { + OpenedRecordInfo( + {required DHTRecordDescriptor recordDescriptor, + required KeyPair? defaultWriter, + required VeilidRoutingContext defaultRoutingContext}) + : shared = SharedDHTRecordData( + recordDescriptor: recordDescriptor, + defaultWriter: defaultWriter, + defaultRoutingContext: defaultRoutingContext); + SharedDHTRecordData shared; + Set records = {}; +} + class DHTRecordPool with TableDBBacked { DHTRecordPool._(Veilid veilid, VeilidRoutingContext routingContext) : _state = DHTRecordPoolAllocations( childrenByParent: IMap(), parentByChild: IMap(), rootRecords: ISet()), - _opened = {}, - _locks = AsyncTagLock(), + _mutex = Mutex(), + _opened = {}, _routingContext = routingContext, _veilid = veilid; // Persistent DHT record list DHTRecordPoolAllocations _state; - // Lock table to ensure we don't open the same record more than once - final AsyncTagLock _locks; + // Create/open Mutex + final Mutex _mutex; // Which DHT records are currently open - final Map _opened; + final Map _opened; // Default routing context to use for new keys final VeilidRoutingContext _routingContext; // Convenience accessor @@ -107,30 +142,106 @@ class DHTRecordPool with TableDBBacked { Veilid get veilid => _veilid; - void _recordOpened(DHTRecord record) { - if (_opened.containsKey(record.key)) { - throw StateError('record already opened'); + Future _recordCreateInner( + {required VeilidRoutingContext dhtctx, + required DHTSchema schema, + KeyPair? writer, + TypedKey? parent}) async { + assert(_mutex.isLocked, 'should be locked here'); + + // Create the record + final recordDescriptor = await dhtctx.createDHTRecord(schema); + + // Reopen if a writer is specified to ensure + // we switch the default writer + if (writer != null) { + await dhtctx.openDHTRecord(recordDescriptor.key, writer: writer); } - _opened[record.key] = record; + final openedRecordInfo = OpenedRecordInfo( + recordDescriptor: recordDescriptor, + defaultWriter: writer ?? recordDescriptor.ownerKeyPair(), + defaultRoutingContext: dhtctx); + _opened[recordDescriptor.key] = openedRecordInfo; + + // Register the dependency + await _addDependencyInner(parent, recordDescriptor.key); + + return openedRecordInfo; } - void recordClosed(TypedKey key) { - final rec = _opened.remove(key); - if (rec == null) { - throw StateError('record already closed'); + Future _recordOpenInner( + {required VeilidRoutingContext dhtctx, + required TypedKey recordKey, + KeyPair? writer, + TypedKey? parent}) async { + assert(_mutex.isLocked, 'should be locked here'); + + // If we are opening a key that already exists + // make sure we are using the same parent if one was specified + _validateParent(parent, recordKey); + + // See if this has been opened yet + final openedRecordInfo = _opened[recordKey]; + if (openedRecordInfo == null) { + // Fresh open, just open the record + final recordDescriptor = + await dhtctx.openDHTRecord(recordKey, writer: writer); + final newOpenedRecordInfo = OpenedRecordInfo( + recordDescriptor: recordDescriptor, + defaultWriter: writer, + defaultRoutingContext: dhtctx); + _opened[recordDescriptor.key] = newOpenedRecordInfo; + + // Register the dependency + await _addDependencyInner(parent, recordKey); + + return newOpenedRecordInfo; } - _locks.unlockTag(key); + + // Already opened + + // See if we need to reopen the record with a default writer and possibly + // a different routing context + if (writer != null && openedRecordInfo.shared.defaultWriter == null) { + final newRecordDescriptor = + await dhtctx.openDHTRecord(recordKey, writer: writer); + openedRecordInfo.shared.defaultWriter = writer; + openedRecordInfo.shared.defaultRoutingContext = dhtctx; + if (openedRecordInfo.shared.recordDescriptor.ownerSecret == null) { + openedRecordInfo.shared.recordDescriptor = newRecordDescriptor; + } + } + + // Register the dependency + await _addDependencyInner(parent, recordKey); + + return openedRecordInfo; } - Future deleteDeep(TypedKey parent) async { - // Collect all dependencies + Future _recordClosed(DHTRecord record) async { + await _mutex.protect(() async { + final key = record.key; + final openedRecordInfo = _opened[key]; + if (openedRecordInfo == null || + !openedRecordInfo.records.remove(record)) { + throw StateError('record already closed'); + } + if (openedRecordInfo.records.isEmpty) { + await _routingContext.closeDHTRecord(key); + _opened.remove(key); + } + }); + } + + Future delete(TypedKey recordKey) async { + // Collect all dependencies (including the record itself) final allDeps = []; - final currentDeps = [parent]; + final currentDeps = [recordKey]; while (currentDeps.isNotEmpty) { final nextDep = currentDeps.removeLast(); // Remove this child from its parent - await _removeDependency(nextDep); + await _removeDependencyInner(nextDep); allDeps.add(nextDep); final childDeps = @@ -138,18 +249,27 @@ class DHTRecordPool with TableDBBacked { currentDeps.addAll(childDeps); } - // Delete all dependent records in parallel - final allFutures = >[]; + // Delete all dependent records in parallel (including the record itself) + final allDeleteFutures = >[]; + final allCloseFutures = >[]; + final allDeletedRecords = {}; for (final dep in allDeps) { // If record is opened, close it first - final rec = _opened[dep]; - if (rec != null) { - await rec.close(); + final openinfo = _opened[dep]; + if (openinfo != null) { + for (final rec in openinfo.records) { + allCloseFutures.add(rec.close()); + allDeletedRecords.add(rec); + } } // Then delete - allFutures.add(_routingContext.deleteDHTRecord(dep)); + allDeleteFutures.add(_routingContext.deleteDHTRecord(dep)); + } + await Future.wait(allCloseFutures); + await Future.wait(allDeleteFutures); + for (final deletedRecord in allDeletedRecords) { + deletedRecord._markDeleted(); } - await Future.wait(allFutures); } void _validateParent(TypedKey? parent, TypedKey child) { @@ -169,7 +289,8 @@ class DHTRecordPool with TableDBBacked { } } - Future _addDependency(TypedKey? parent, TypedKey child) async { + Future _addDependencyInner(TypedKey? parent, TypedKey child) async { + assert(_mutex.isLocked, 'should be locked here'); if (parent == null) { if (_state.rootRecords.contains(child)) { // Dependency already added @@ -191,7 +312,8 @@ class DHTRecordPool with TableDBBacked { } } - Future _removeDependency(TypedKey child) async { + Future _removeDependencyInner(TypedKey child) async { + assert(_mutex.isLocked, 'should be locked here'); if (_state.rootRecords.contains(child)) { _state = await store( _state.copyWith(rootRecords: _state.rootRecords.remove(child))); @@ -226,57 +348,52 @@ class DHTRecordPool with TableDBBacked { int defaultSubkey = 0, DHTRecordCrypto? crypto, KeyPair? writer, - }) async { - final dhtctx = routingContext ?? _routingContext; - final recordDescriptor = await dhtctx.createDHTRecord(schema); + }) async => + _mutex.protect(() async { + final dhtctx = routingContext ?? _routingContext; - await _locks.lockTag(recordDescriptor.key); + final openedRecordInfo = await _recordCreateInner( + dhtctx: dhtctx, schema: schema, writer: writer, parent: parent); - final rec = DHTRecord( - routingContext: dhtctx, - recordDescriptor: recordDescriptor, - defaultSubkey: defaultSubkey, - writer: writer ?? recordDescriptor.ownerKeyPair(), - crypto: crypto ?? - await DHTRecordCryptoPrivate.fromTypedKeyPair( - recordDescriptor.ownerTypedKeyPair()!)); + final rec = DHTRecord( + routingContext: dhtctx, + defaultSubkey: defaultSubkey, + sharedDHTRecordData: openedRecordInfo.shared, + writer: writer ?? + openedRecordInfo.shared.recordDescriptor.ownerKeyPair(), + crypto: crypto ?? + await DHTRecordCryptoPrivate.fromTypedKeyPair(openedRecordInfo + .shared.recordDescriptor + .ownerTypedKeyPair()!)); - await _addDependency(parent, rec.key); + openedRecordInfo.records.add(rec); - _recordOpened(rec); - - return rec; - } + return rec; + }); /// Open a DHTRecord readonly Future openRead(TypedKey recordKey, - {VeilidRoutingContext? routingContext, - TypedKey? parent, - int defaultSubkey = 0, - DHTRecordCrypto? crypto}) async { - await _locks.lockTag(recordKey); + {VeilidRoutingContext? routingContext, + TypedKey? parent, + int defaultSubkey = 0, + DHTRecordCrypto? crypto}) async => + _mutex.protect(() async { + final dhtctx = routingContext ?? _routingContext; - final dhtctx = routingContext ?? _routingContext; + final openedRecordInfo = await _recordOpenInner( + dhtctx: dhtctx, recordKey: recordKey, parent: parent); - late final DHTRecord rec; - // If we are opening a key that already exists - // make sure we are using the same parent if one was specified - _validateParent(parent, recordKey); + final rec = DHTRecord( + routingContext: dhtctx, + defaultSubkey: defaultSubkey, + sharedDHTRecordData: openedRecordInfo.shared, + writer: null, + crypto: crypto ?? const DHTRecordCryptoPublic()); - // Open from the veilid api - final recordDescriptor = await dhtctx.openDHTRecord(recordKey, null); - rec = DHTRecord( - routingContext: dhtctx, - recordDescriptor: recordDescriptor, - defaultSubkey: defaultSubkey, - crypto: crypto ?? const DHTRecordCryptoPublic()); + openedRecordInfo.records.add(rec); - // Register the dependency - await _addDependency(parent, rec.key); - _recordOpened(rec); - - return rec; - } + return rec; + }); /// Open a DHTRecord writable Future openWrite( @@ -286,33 +403,29 @@ class DHTRecordPool with TableDBBacked { TypedKey? parent, int defaultSubkey = 0, DHTRecordCrypto? crypto, - }) async { - await _locks.lockTag(recordKey); + }) async => + _mutex.protect(() async { + final dhtctx = routingContext ?? _routingContext; - final dhtctx = routingContext ?? _routingContext; + final openedRecordInfo = await _recordOpenInner( + dhtctx: dhtctx, + recordKey: recordKey, + parent: parent, + writer: writer); - late final DHTRecord rec; - // If we are opening a key that already exists - // make sure we are using the same parent if one was specified - _validateParent(parent, recordKey); + final rec = DHTRecord( + routingContext: dhtctx, + defaultSubkey: defaultSubkey, + writer: writer, + sharedDHTRecordData: openedRecordInfo.shared, + crypto: crypto ?? + await DHTRecordCryptoPrivate.fromTypedKeyPair( + TypedKeyPair.fromKeyPair(recordKey.kind, writer))); - // Open from the veilid api - final recordDescriptor = await dhtctx.openDHTRecord(recordKey, writer); - rec = DHTRecord( - routingContext: dhtctx, - recordDescriptor: recordDescriptor, - defaultSubkey: defaultSubkey, - writer: writer, - crypto: crypto ?? - await DHTRecordCryptoPrivate.fromTypedKeyPair( - TypedKeyPair.fromKeyPair(recordKey.kind, writer))); + openedRecordInfo.records.add(rec); - // Register the dependency if specified - await _addDependency(parent, rec.key); - _recordOpened(rec); - - return rec; - } + return rec; + }); /// Open a DHTRecord owned /// This is the same as writable but uses an OwnedDHTRecordPointer @@ -336,9 +449,6 @@ class DHTRecordPool with TableDBBacked { crypto: crypto, ); - /// Look up an opened DHTRecord - DHTRecord? getOpenedRecord(TypedKey recordKey) => _opened[recordKey]; - /// Get the parent of a DHTRecord key if it exists TypedKey? getParentRecordKey(TypedKey child) { final childJson = child.toJson(); @@ -351,33 +461,107 @@ class DHTRecordPool with TableDBBacked { // Change for (final kv in _opened.entries) { if (kv.key == updateValueChange.key) { - kv.value.addRemoteValueChange(updateValueChange); + for (final rec in kv.value.records) { + rec.addRemoteValueChange(updateValueChange); + } break; } } } else { + final now = Veilid.instance.now().value; // Expired, process renewal if desired - for (final kv in _opened.entries) { - if (kv.key == updateValueChange.key) { - // Renew watch state - kv.value.needsWatchStateUpdate = true; + for (final entry in _opened.entries) { + final openedKey = entry.key; + final openedRecordInfo = entry.value; - // See if the watch had an expiration and if it has expired - // otherwise the renewal will keep the same parameters - final watchState = kv.value.watchState; - if (watchState != null) { - final exp = watchState.expiration; - if (exp != null && exp.value < Veilid.instance.now().value) { - // Has expiration, and it has expired, clear watch state - kv.value.watchState = null; + if (openedKey == updateValueChange.key) { + // Renew watch state for each opened recrod + for (final rec in openedRecordInfo.records) { + // See if the watch had an expiration and if it has expired + // otherwise the renewal will keep the same parameters + final watchState = rec.watchState; + if (watchState != null) { + final exp = watchState.expiration; + if (exp != null && exp.value < now) { + // Has expiration, and it has expired, clear watch state + rec.watchState = null; + } } } + openedRecordInfo.shared.needsWatchStateUpdate = true; break; } } } } + WatchState? _collectUnionWatchState(Iterable records) { + // Collect union of opened record watch states + int? totalCount; + Timestamp? maxExpiration; + List? allSubkeys; + + var noExpiration = false; + var everySubkey = false; + var cancelWatch = true; + + for (final rec in records) { + final ws = rec.watchState; + if (ws != null) { + cancelWatch = false; + final wsCount = ws.count; + if (wsCount != null) { + totalCount = totalCount ?? 0 + min(wsCount, 0x7FFFFFFF); + totalCount = min(totalCount, 0x7FFFFFFF); + } + final wsExp = ws.expiration; + if (wsExp != null && !noExpiration) { + maxExpiration = maxExpiration == null + ? wsExp + : wsExp.value > maxExpiration.value + ? wsExp + : maxExpiration; + } else { + noExpiration = true; + } + final wsSubkeys = ws.subkeys; + if (wsSubkeys != null && !everySubkey) { + allSubkeys = allSubkeys == null + ? wsSubkeys + : allSubkeys.unionSubkeys(wsSubkeys); + } else { + everySubkey = true; + } + } + } + if (noExpiration) { + maxExpiration = null; + } + if (everySubkey) { + allSubkeys = null; + } + if (cancelWatch) { + return null; + } + + return WatchState( + subkeys: allSubkeys, expiration: maxExpiration, count: totalCount); + } + + void _updateWatchExpirations( + Iterable records, Timestamp realExpiration) { + for (final rec in records) { + final ws = rec.watchState; + if (ws != null) { + rec.watchState = WatchState( + subkeys: ws.subkeys, + expiration: ws.expiration, + count: ws.count, + realExpiration: realExpiration); + } + } + } + /// Ticker to check watch state change requests Future tick() async { if (inTick) { @@ -386,53 +570,55 @@ class DHTRecordPool with TableDBBacked { inTick = true; try { // See if any opened records need watch state changes - final unord = List>.empty(growable: true); + final unord = >[]; for (final kv in _opened.entries) { + final openedRecordKey = kv.key; + final openedRecordInfo = kv.value; + final dhtctx = openedRecordInfo.shared.defaultRoutingContext; + // Check if already updating - if (kv.value.inWatchStateUpdate) { + if (openedRecordInfo.shared.inWatchStateUpdate) { continue; } - if (kv.value.needsWatchStateUpdate) { - kv.value.inWatchStateUpdate = true; + if (openedRecordInfo.shared.needsWatchStateUpdate) { + openedRecordInfo.shared.inWatchStateUpdate = true; - final ws = kv.value.watchState; - if (ws == null) { + final watchState = _collectUnionWatchState(openedRecordInfo.records); + + // Apply watch changes for record + if (watchState == null) { unord.add(() async { // Record needs watch cancel try { - final done = - await kv.value.routingContext.cancelDHTWatch(kv.key); + final done = await dhtctx.cancelDHTWatch(openedRecordKey); assert(done, 'should always be done when cancelling whole subkey range'); - kv.value.needsWatchStateUpdate = false; + openedRecordInfo.shared.needsWatchStateUpdate = false; } on VeilidAPIException { // Failed to cancel DHT watch, try again next tick } - kv.value.inWatchStateUpdate = false; + openedRecordInfo.shared.inWatchStateUpdate = false; }()); } else { unord.add(() async { // Record needs new watch try { - final realExpiration = await kv.value.routingContext - .watchDHTValues(kv.key, - subkeys: ws.subkeys?.toList(), - count: ws.count, - expiration: ws.expiration); - kv.value.needsWatchStateUpdate = false; + final realExpiration = await dhtctx.watchDHTValues( + openedRecordKey, + subkeys: watchState.subkeys?.toList(), + count: watchState.count, + expiration: watchState.expiration); + openedRecordInfo.shared.needsWatchStateUpdate = false; - // Update watch state with real expiration - kv.value.watchState = WatchState( - subkeys: ws.subkeys, - expiration: ws.expiration, - count: ws.count, - realExpiration: realExpiration); + // Update watch states with real expiration + _updateWatchExpirations( + openedRecordInfo.records, realExpiration); } on VeilidAPIException { // Failed to cancel DHT watch, try again next tick } - kv.value.inWatchStateUpdate = false; + openedRecordInfo.shared.inWatchStateUpdate = false; }()); } } diff --git a/packages/veilid_support/lib/dht_support/src/dht_short_array.dart b/packages/veilid_support/lib/dht_support/src/dht_short_array.dart index 11af592..b54221a 100644 --- a/packages/veilid_support/lib/dht_support/src/dht_short_array.dart +++ b/packages/veilid_support/lib/dht_support/src/dht_short_array.dart @@ -86,16 +86,12 @@ class DHTShortArray { final schema = DHTSchema.smpl( oCnt: 0, members: [DHTSchemaMember(mKey: smplWriter.key, mCnt: stride + 1)]); - final dhtCreateRecord = await pool.create( + dhtRecord = await pool.create( parent: parent, routingContext: routingContext, schema: schema, crypto: crypto, writer: smplWriter); - // Reopen with SMPL writer - await dhtCreateRecord.close(); - dhtRecord = await pool.openWrite(dhtCreateRecord.key, smplWriter, - parent: parent, routingContext: routingContext, crypto: crypto); } else { final schema = DHTSchema.dflt(oCnt: stride + 1); dhtRecord = await pool.create( From ce4e19f88dc9fbb8fce8adbf5e45df21cfd2d0cd Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Sun, 25 Feb 2024 10:03:41 -0500 Subject: [PATCH 45/68] clean up single-record open remnants --- .../account_repository.dart | 72 ++++++++++++------- .../lib/dht_support/src/dht_record_pool.dart | 1 - 2 files changed, 45 insertions(+), 28 deletions(-) diff --git a/lib/account_manager/repository/account_repository/account_repository.dart b/lib/account_manager/repository/account_repository/account_repository.dart index 40ce315..872393e 100644 --- a/lib/account_manager/repository/account_repository/account_repository.dart +++ b/lib/account_manager/repository/account_repository/account_repository.dart @@ -42,10 +42,14 @@ class AccountRepository { valueFromJson: (obj) => obj == null ? null : TypedKey.fromJson(obj), valueToJson: (val) => val?.toJson()); + ////////////////////////////////////////////////////////////// + /// Fields + final TableDBValue> _localAccounts; final TableDBValue> _userLogins; final TableDBValue _activeLocalAccount; final StreamController _streamController; + final Map _openedAccountRecords = {}; ////////////////////////////////////////////////////////////// /// Singleton initialization @@ -59,6 +63,10 @@ class AccountRepository { await _openLoggedInDHTRecords(); } + Future close() async { + await _closeLoggedInDHTRecords(); + } + ////////////////////////////////////////////////////////////// /// Streams @@ -132,9 +140,8 @@ class AccountRepository { } // Pull the account DHT key, decode it and return it - final pool = DHTRecordPool.instance; - final accountRecord = pool - .getOpenedRecord(userLogin.accountRecordInfo.accountRecord.recordKey); + final accountRecord = + _getAccountRecord(userLogin.accountRecordInfo.accountRecord.recordKey); if (accountRecord == null) { // Account could not be read or decrypted from DHT return AccountInfo( @@ -367,7 +374,6 @@ class AccountRepository { Future logout(TypedKey? accountMasterRecordKey) async { // Resolve which user to log out - //final userLogins = await _userLogins.get(); final activeLocalAccount = await _activeLocalAccount.get(); final logoutUser = accountMasterRecordKey ?? activeLocalAccount; if (logoutUser == null) { @@ -382,10 +388,9 @@ class AccountRepository { } // Close DHT records for this account - final pool = DHTRecordPool.instance; final accountRecordKey = logoutUserLogin.accountRecordInfo.accountRecord.recordKey; - final accountRecord = pool.getOpenedRecord(accountRecordKey); + final accountRecord = _openedAccountRecords.remove(accountRecordKey); await accountRecord?.close(); // Remove user from active logins list @@ -396,39 +401,52 @@ class AccountRepository { } Future _openLoggedInDHTRecords() async { - final pool = DHTRecordPool.instance; - // For all user logins if they arent open yet final userLogins = await _userLogins.get(); for (final userLogin in userLogins) { - //// Account record key ///////////////////////////// - final accountRecordKey = - userLogin.accountRecordInfo.accountRecord.recordKey; - final existingAccountRecord = pool.getOpenedRecord(accountRecordKey); - if (existingAccountRecord == null) { - final localAccount = - fetchLocalAccount(userLogin.accountMasterRecordKey); - - // Record not yet open, do it - final record = await pool.openOwned( - userLogin.accountRecordInfo.accountRecord, - parent: localAccount!.identityMaster.identityRecordKey); - // Watch the record's only (default) key - await record.watch(); - } + await _openAccountRecord(userLogin); } } Future _closeLoggedInDHTRecords() async { - final pool = DHTRecordPool.instance; - final userLogins = await _userLogins.get(); for (final userLogin in userLogins) { //// Account record key ///////////////////////////// final accountRecordKey = userLogin.accountRecordInfo.accountRecord.recordKey; - final accountRecord = pool.getOpenedRecord(accountRecordKey); - await accountRecord?.close(); + await _closeAccountRecord(accountRecordKey); } } + + Future _openAccountRecord(UserLogin userLogin) async { + final accountRecordKey = + userLogin.accountRecordInfo.accountRecord.recordKey; + + final existingAccountRecord = _openedAccountRecords[accountRecordKey]; + if (existingAccountRecord != null) { + return existingAccountRecord; + } + final localAccount = fetchLocalAccount(userLogin.accountMasterRecordKey)!; + + // Record not yet open, do it + final pool = DHTRecordPool.instance; + final record = await pool.openOwned( + userLogin.accountRecordInfo.accountRecord, + parent: localAccount.identityMaster.identityRecordKey); + + _openedAccountRecords[accountRecordKey] = record; + + // Watch the record's only (default) key + await record.watch(); + + return record; + } + + DHTRecord? _getAccountRecord(TypedKey accountRecordKey) => + _openedAccountRecords[accountRecordKey]; + + Future _closeAccountRecord(TypedKey accountRecordKey) async { + final accountRecord = _openedAccountRecords.remove(accountRecordKey); + await accountRecord?.close(); + } } diff --git a/packages/veilid_support/lib/dht_support/src/dht_record_pool.dart b/packages/veilid_support/lib/dht_support/src/dht_record_pool.dart index 736329e..bdc4946 100644 --- a/packages/veilid_support/lib/dht_support/src/dht_record_pool.dart +++ b/packages/veilid_support/lib/dht_support/src/dht_record_pool.dart @@ -2,7 +2,6 @@ import 'dart:async'; import 'dart:math'; import 'dart:typed_data'; -import 'package:async_tools/async_tools.dart'; import 'package:equatable/equatable.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; From 8e7619677a6e876e7b61599d16af2aa9d0b25f59 Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Sun, 25 Feb 2024 22:52:09 -0500 Subject: [PATCH 46/68] chat work --- .../cubits/contact_invitation_list_cubit.dart | 106 +--- .../cubits/contact_request_inbox_cubit.dart | 110 +---- .../cubits/waiting_invitation_cubit.dart | 111 ----- .../models/valid_contact_invitation.dart | 1 + .../repository/processor_repository.dart | 2 +- .../lib/dht_support/src/dht_record.dart | 19 +- .../lib/dht_support/src/dht_record_cubit.dart | 25 +- .../lib/dht_support/src/dht_record_pool.dart | 174 +++---- .../lib/dht_support/src/dht_short_array.dart | 455 +++++++++--------- .../src/dht_short_array_cubit.dart | 26 +- 10 files changed, 372 insertions(+), 657 deletions(-) diff --git a/lib/contact_invitation/cubits/contact_invitation_list_cubit.dart b/lib/contact_invitation/cubits/contact_invitation_list_cubit.dart index 7deacf1..49bc4a6 100644 --- a/lib/contact_invitation/cubits/contact_invitation_list_cubit.dart +++ b/lib/contact_invitation/cubits/contact_invitation_list_cubit.dart @@ -101,6 +101,8 @@ class ContactInvitationListCubit ..private = encryptedContactRequestPrivate; // Create DHT unicast inbox for ContactRequest + // Subkey 0 is the ContactRequest from the initiator + // Subkey 1 will contain the invitation response accept/reject eventually await (await pool.create( parent: _activeAccountInfo.accountRecordKey, schema: DHTSchema.smpl(oCnt: 1, members: [ @@ -262,110 +264,6 @@ class ContactInvitationListCubit return out; } - // Future checkInvitationStatus( - // {required proto.ContactInvitationRecord contactInvitationRecord}) async { - // // Open the contact request inbox - // try { - // final pool = DHTRecordPool.instance; - // final accountRecordKey = _activeAccountInfo - // .userLogin.accountRecordInfo.accountRecord.recordKey; - // final writerKey = contactInvitationRecord.writerKey.toVeilid(); - // final writerSecret = contactInvitationRecord.writerSecret.toVeilid(); - // final recordKey = - // contactInvitationRecord.contactRequestInbox.recordKey.toVeilid(); - // final writer = TypedKeyPair( - // kind: recordKey.kind, key: writerKey, secret: writerSecret); - // final acceptReject = await (await pool.openRead(recordKey, - // crypto: await DHTRecordCryptoPrivate.fromTypedKeyPair(writer), - // parent: accountRecordKey, - // defaultSubkey: 1)) - // .scope((contactRequestInbox) async { - // // - // final signedContactResponse = await contactRequestInbox.getProtobuf( - // proto.SignedContactResponse.fromBuffer, - // forceRefresh: true); - // if (signedContactResponse == null) { - // return null; - // } - - // final contactResponseBytes = - // Uint8List.fromList(signedContactResponse.contactResponse); - // final contactResponse = - // proto.ContactResponse.fromBuffer(contactResponseBytes); - // final contactIdentityMasterRecordKey = - // contactResponse.identityMasterRecordKey.toVeilid(); - // final cs = await pool.veilid.getCryptoSystem(recordKey.kind); - - // // Fetch the remote contact's account master - // final contactIdentityMaster = await openIdentityMaster( - // identityMasterRecordKey: contactIdentityMasterRecordKey); - - // // Verify - // final signature = signedContactResponse.identitySignature.toVeilid(); - // await cs.verify(contactIdentityMaster.identityPublicKey, - // contactResponseBytes, signature); - - // // Check for rejection - // if (!contactResponse.accept) { - // return const InvitationStatus(acceptedContact: null); - // } - - // // Pull profile from remote conversation key - // final remoteConversationRecordKey = - // contactResponse.remoteConversationRecordKey.toVeilid(); - - // final conversation = ConversationCubit( - // activeAccountInfo: _activeAccountInfo, - // remoteIdentityPublicKey: - // contactIdentityMaster.identityPublicTypedKey(), - // remoteConversationRecordKey: remoteConversationRecordKey); - // await conversation.refresh(); - - // final remoteConversation = - // conversation.state.data?.value.remoteConversation; - // if (remoteConversation == null) { - // log.info('Remote conversation could not be read. Waiting...'); - // return null; - // } - - // // Complete the local conversation now that we have the remote profile - // final localConversationRecordKey = - // contactInvitationRecord.localConversationRecordKey.toVeilid(); - // return conversation.initLocalConversation( - // existingConversationRecordKey: localConversationRecordKey, - // profile: _account.profile, - // // ignore: prefer_expression_function_bodies - // callback: (localConversation) async { - // return InvitationStatus( - // acceptedContact: AcceptedContact( - // remoteProfile: remoteConversation.profile, - // remoteIdentity: contactIdentityMaster, - // remoteConversationRecordKey: remoteConversationRecordKey, - // localConversationRecordKey: localConversationRecordKey)); - // }); - // }); - - // if (acceptReject == null) { - // return null; - // } - - // // Delete invitation and return the accepted or rejected contact - // await deleteInvitation( - // accepted: acceptReject.acceptedContact != null, - // contactInvitationRecord: contactInvitationRecord); - - // return acceptReject; - // } on Exception catch (e) { - // log.error('Exception in checkInvitationStatus: $e', e); - - // // Attempt to clean up. All this needs better lifetime management - // await deleteInvitation( - // accepted: false, contactInvitationRecord: contactInvitationRecord); - - // rethrow; - // } - // } - // final ActiveAccountInfo _activeAccountInfo; final proto.Account _account; diff --git a/lib/contact_invitation/cubits/contact_request_inbox_cubit.dart b/lib/contact_invitation/cubits/contact_request_inbox_cubit.dart index eea29ec..80fa6e7 100644 --- a/lib/contact_invitation/cubits/contact_request_inbox_cubit.dart +++ b/lib/contact_invitation/cubits/contact_request_inbox_cubit.dart @@ -3,6 +3,7 @@ import 'package:veilid_support/veilid_support.dart'; import '../../account_manager/account_manager.dart'; import '../../proto/proto.dart' as proto; +// Watch subkey #1 of the ContactRequest record for accept/reject class ContactRequestInboxCubit extends DefaultDHTRecordCubit { ContactRequestInboxCubit( @@ -40,112 +41,3 @@ class ContactRequestInboxCubit final ActiveAccountInfo activeAccountInfo; final proto.ContactInvitationRecord contactInvitationRecord; } - // Future checkInvitationStatus( - // {}) async { - // // Open the contact request inbox - // try { - // final pool = DHTRecordPool.instance; - // final accountRecordKey = _activeAccountInfo - // .userLogin.accountRecordInfo.accountRecord.recordKey; - // final writerKey = contactInvitationRecord.writerKey.toVeilid(); - // final writerSecret = contactInvitationRecord.writerSecret.toVeilid(); - // final recordKey = - // contactInvitationRecord.contactRequestInbox.recordKey.toVeilid(); - // final writer = TypedKeyPair( - // kind: recordKey.kind, key: writerKey, secret: writerSecret); - // final acceptReject = await (await pool.openRead(recordKey, - // crypto: await DHTRecordCryptoPrivate.fromTypedKeyPair(writer), - // parent: accountRecordKey, - // defaultSubkey: 1)) - // .scope((contactRequestInbox) async { - // // - // final signedContactResponse = await contactRequestInbox.getProtobuf( - // proto.SignedContactResponse.fromBuffer, - // forceRefresh: true); - // if (signedContactResponse == null) { - // return null; - // } - - // final contactResponseBytes = - // Uint8List.fromList(signedContactResponse.contactResponse); - // final contactResponse = - // proto.ContactResponse.fromBuffer(contactResponseBytes); - // final contactIdentityMasterRecordKey = - // contactResponse.identityMasterRecordKey.toVeilid(); - // final cs = await pool.veilid.getCryptoSystem(recordKey.kind); - - // // Fetch the remote contact's account master - // final contactIdentityMaster = await openIdentityMaster( - // identityMasterRecordKey: contactIdentityMasterRecordKey); - - // // Verify - // final signature = signedContactResponse.identitySignature.toVeilid(); - // await cs.verify(contactIdentityMaster.identityPublicKey, - // contactResponseBytes, signature); - - // // Check for rejection - // if (!contactResponse.accept) { - // return const InvitationStatus(acceptedContact: null); - // } - - // // Pull profile from remote conversation key - // final remoteConversationRecordKey = - // contactResponse.remoteConversationRecordKey.toVeilid(); - - // final conversation = ConversationCubit( - // activeAccountInfo: _activeAccountInfo, - // remoteIdentityPublicKey: - // contactIdentityMaster.identityPublicTypedKey(), - // remoteConversationRecordKey: remoteConversationRecordKey); - // await conversation.refresh(); - - // final remoteConversation = - // conversation.state.data?.value.remoteConversation; - // if (remoteConversation == null) { - // log.info('Remote conversation could not be read. Waiting...'); - // return null; - // } - - // // Complete the local conversation now that we have the remote profile - // final localConversationRecordKey = - // contactInvitationRecord.localConversationRecordKey.toVeilid(); - // return conversation.initLocalConversation( - // existingConversationRecordKey: localConversationRecordKey, - // profile: _account.profile, - // // ignore: prefer_expression_function_bodies - // callback: (localConversation) async { - // return InvitationStatus( - // acceptedContact: AcceptedContact( - // remoteProfile: remoteConversation.profile, - // remoteIdentity: contactIdentityMaster, - // remoteConversationRecordKey: remoteConversationRecordKey, - // localConversationRecordKey: localConversationRecordKey)); - // }); - // }); - - // if (acceptReject == null) { - // return null; - // } - - // // Delete invitation and return the accepted or rejected contact - // await deleteInvitation( - // accepted: acceptReject.acceptedContact != null, - // contactInvitationRecord: contactInvitationRecord); - - // return acceptReject; - // } on Exception catch (e) { - // log.error('Exception in checkInvitationStatus: $e', e); - - // // Attempt to clean up. All this needs better lifetime management - // await deleteInvitation( - // accepted: false, contactInvitationRecord: contactInvitationRecord); - - // rethrow; - // } - - - - - - - diff --git a/lib/contact_invitation/cubits/waiting_invitation_cubit.dart b/lib/contact_invitation/cubits/waiting_invitation_cubit.dart index 44e0f36..0c618c0 100644 --- a/lib/contact_invitation/cubits/waiting_invitation_cubit.dart +++ b/lib/contact_invitation/cubits/waiting_invitation_cubit.dart @@ -108,114 +108,3 @@ class WaitingInvitationCubit extends AsyncTransformerCubit checkInvitationStatus( - // {}) async { - // // Open the contact request inbox - // try { - // final pool = DHTRecordPool.instance; - // final accountRecordKey = _activeAccountInfo - // .userLogin.accountRecordInfo.accountRecord.recordKey; - // final writerKey = contactInvitationRecord.writerKey.toVeilid(); - // final writerSecret = contactInvitationRecord.writerSecret.toVeilid(); - // final recordKey = - // contactInvitationRecord.contactRequestInbox.recordKey.toVeilid(); - // final writer = TypedKeyPair( - // kind: recordKey.kind, key: writerKey, secret: writerSecret); - // final acceptReject = await (await pool.openRead(recordKey, - // crypto: await DHTRecordCryptoPrivate.fromTypedKeyPair(writer), - // parent: accountRecordKey, - // defaultSubkey: 1)) - // .scope((contactRequestInbox) async { - // // - // final signedContactResponse = await contactRequestInbox.getProtobuf( - // proto.SignedContactResponse.fromBuffer, - // forceRefresh: true); - // if (signedContactResponse == null) { - // return null; - // } - - // final contactResponseBytes = - // Uint8List.fromList(signedContactResponse.contactResponse); - // final contactResponse = - // proto.ContactResponse.fromBuffer(contactResponseBytes); - // final contactIdentityMasterRecordKey = - // contactResponse.identityMasterRecordKey.toVeilid(); - // final cs = await pool.veilid.getCryptoSystem(recordKey.kind); - - // // Fetch the remote contact's account master - // final contactIdentityMaster = await openIdentityMaster( - // identityMasterRecordKey: contactIdentityMasterRecordKey); - - // // Verify - // final signature = signedContactResponse.identitySignature.toVeilid(); - // await cs.verify(contactIdentityMaster.identityPublicKey, - // contactResponseBytes, signature); - - // // Check for rejection - // if (!contactResponse.accept) { - // return const InvitationStatus(acceptedContact: null); - // } - - // // Pull profile from remote conversation key - // final remoteConversationRecordKey = - // contactResponse.remoteConversationRecordKey.toVeilid(); - - // final conversation = ConversationCubit( - // activeAccountInfo: _activeAccountInfo, - // remoteIdentityPublicKey: - // contactIdentityMaster.identityPublicTypedKey(), - // remoteConversationRecordKey: remoteConversationRecordKey); - // await conversation.refresh(); - - // final remoteConversation = - // conversation.state.data?.value.remoteConversation; - // if (remoteConversation == null) { - // log.info('Remote conversation could not be read. Waiting...'); - // return null; - // } - - // // Complete the local conversation now that we have the remote profile - // final localConversationRecordKey = - // contactInvitationRecord.localConversationRecordKey.toVeilid(); - // return conversation.initLocalConversation( - // existingConversationRecordKey: localConversationRecordKey, - // profile: _account.profile, - // // ignore: prefer_expression_function_bodies - // callback: (localConversation) async { - // return InvitationStatus( - // acceptedContact: AcceptedContact( - // remoteProfile: remoteConversation.profile, - // remoteIdentity: contactIdentityMaster, - // remoteConversationRecordKey: remoteConversationRecordKey, - // localConversationRecordKey: localConversationRecordKey)); - // }); - // }); - - // if (acceptReject == null) { - // return null; - // } - - // // Delete invitation and return the accepted or rejected contact - // await deleteInvitation( - // accepted: acceptReject.acceptedContact != null, - // contactInvitationRecord: contactInvitationRecord); - - // return acceptReject; - // } on Exception catch (e) { - // log.error('Exception in checkInvitationStatus: $e', e); - - // // Attempt to clean up. All this needs better lifetime management - // await deleteInvitation( - // accepted: false, contactInvitationRecord: contactInvitationRecord); - - // rethrow; - // } - - - - - - - diff --git a/lib/contact_invitation/models/valid_contact_invitation.dart b/lib/contact_invitation/models/valid_contact_invitation.dart index d4db157..88f43d9 100644 --- a/lib/contact_invitation/models/valid_contact_invitation.dart +++ b/lib/contact_invitation/models/valid_contact_invitation.dart @@ -32,6 +32,7 @@ class ValidContactInvitation { final pool = DHTRecordPool.instance; try { // Ensure we don't delete this if we're trying to chat to self + // The initiating side will delete the records in deleteInvitation() final isSelf = _contactIdentityMaster.identityPublicKey == _activeAccountInfo.localAccount.identityMaster.identityPublicKey; final accountRecordKey = _activeAccountInfo.accountRecordKey; diff --git a/lib/veilid_processor/repository/processor_repository.dart b/lib/veilid_processor/repository/processor_repository.dart index fa99a95..0a17fc7 100644 --- a/lib/veilid_processor/repository/processor_repository.dart +++ b/lib/veilid_processor/repository/processor_repository.dart @@ -120,7 +120,7 @@ class ProcessorRepository { void processUpdateValueChange(VeilidUpdateValueChange updateValueChange) { // Send value updates to DHTRecordPool - DHTRecordPool.instance.processUpdateValueChange(updateValueChange); + DHTRecordPool.instance.processRemoteValueChange(updateValueChange); } //////////////////////////////////////////// diff --git a/packages/veilid_support/lib/dht_support/src/dht_record.dart b/packages/veilid_support/lib/dht_support/src/dht_record.dart index a94aa3a..325d6e8 100644 --- a/packages/veilid_support/lib/dht_support/src/dht_record.dart +++ b/packages/veilid_support/lib/dht_support/src/dht_record.dart @@ -161,8 +161,8 @@ class DHTRecord { final encryptedNewValue = await _crypto.encrypt(newValue, subkey); // Set the new data if possible - var newValueData = - await _routingContext.setDHTValue(key, subkey, encryptedNewValue); + var newValueData = await _routingContext + .setDHTValue(key, subkey, encryptedNewValue, writer: _writer); if (newValueData == null) { // A newer value wasn't found on the set, but // we may get a newer value when getting the value for the sequence number @@ -181,7 +181,7 @@ class DHTRecord { // if so, shortcut and don't bother decrypting it if (newValueData.data.equals(encryptedNewValue)) { if (isUpdated) { - _addLocalValueChange(newValue, subkey); + DHTRecordPool.instance.processLocalValueChange(key, newValue, subkey); } return null; } @@ -189,7 +189,8 @@ class DHTRecord { // Decrypt value to return it final decryptedNewValue = await _crypto.decrypt(newValueData.data, subkey); if (isUpdated) { - _addLocalValueChange(decryptedNewValue, subkey); + DHTRecordPool.instance + .processLocalValueChange(key, decryptedNewValue, subkey); } return decryptedNewValue; } @@ -203,8 +204,8 @@ class DHTRecord { do { do { // Set the new data - newValueData = - await _routingContext.setDHTValue(key, subkey, encryptedNewValue); + newValueData = await _routingContext + .setDHTValue(key, subkey, encryptedNewValue, writer: _writer); // Repeat if newer data on the network was found } while (newValueData != null); @@ -226,7 +227,7 @@ class DHTRecord { final isUpdated = newValueData.seq != lastSeq; if (isUpdated) { - _addLocalValueChange(newValue, subkey); + DHTRecordPool.instance.processLocalValueChange(key, newValue, subkey); } } @@ -356,7 +357,7 @@ class DHTRecord { if (watchedSubkeys == null) { // Report all subkeys watchController?.add( - DHTRecordWatchChange(local: false, data: data, subkeys: subkeys)); + DHTRecordWatchChange(local: local, data: data, subkeys: subkeys)); } else { // Only some subkeys are being watched, see if the reported update // overlaps the subkeys being watched @@ -382,7 +383,7 @@ class DHTRecord { local: true, data: data, subkeys: [ValueSubkeyRange.single(subkey)]); } - void addRemoteValueChange(VeilidUpdateValueChange update) { + void _addRemoteValueChange(VeilidUpdateValueChange update) { _addValueChange( local: false, data: update.valueData.data, subkeys: update.subkeys); } diff --git a/packages/veilid_support/lib/dht_support/src/dht_record_cubit.dart b/packages/veilid_support/lib/dht_support/src/dht_record_cubit.dart index 4a8eba0..176798f 100644 --- a/packages/veilid_support/lib/dht_support/src/dht_record_cubit.dart +++ b/packages/veilid_support/lib/dht_support/src/dht_record_cubit.dart @@ -9,12 +9,14 @@ import '../../veilid_support.dart'; typedef InitialStateFunction = Future Function(DHTRecord); typedef StateFunction = Future Function( DHTRecord, List, Uint8List?); +typedef WatchFunction = Future Function(DHTRecord); class DHTRecordCubit extends Cubit> { DHTRecordCubit({ required Future Function() open, required InitialStateFunction initialStateFunction, required StateFunction stateFunction, + required WatchFunction watchFunction, }) : _wantsCloseRecord = false, _stateFunction = stateFunction, super(const AsyncValue.loading()) { @@ -22,7 +24,7 @@ class DHTRecordCubit extends Cubit> { // Do record open/create _record = await open(); _wantsCloseRecord = true; - await _init(initialStateFunction, stateFunction); + await _init(initialStateFunction, stateFunction, watchFunction); }); } @@ -30,18 +32,20 @@ class DHTRecordCubit extends Cubit> { required DHTRecord record, required InitialStateFunction initialStateFunction, required StateFunction stateFunction, + required WatchFunction watchFunction, }) : _record = record, _stateFunction = stateFunction, _wantsCloseRecord = false, super(const AsyncValue.loading()) { Future.delayed(Duration.zero, () async { - await _init(initialStateFunction, stateFunction); + await _init(initialStateFunction, stateFunction, watchFunction); }); } Future _init( InitialStateFunction initialStateFunction, StateFunction stateFunction, + WatchFunction watchFunction, ) async { // Make initial state update try { @@ -63,10 +67,13 @@ class DHTRecordCubit extends Cubit> { emit(AsyncValue.error(e)); } }); + + await watchFunction(_record); } @override Future close() async { + await _record.cancelWatch(); await _subscription?.cancel(); _subscription = null; if (_wantsCloseRecord) { @@ -113,15 +120,16 @@ class DefaultDHTRecordCubit extends DHTRecordCubit { required T Function(List data) decodeState, }) : super( initialStateFunction: _makeInitialStateFunction(decodeState), - stateFunction: _makeStateFunction(decodeState)); + stateFunction: _makeStateFunction(decodeState), + watchFunction: _makeWatchFunction()); DefaultDHTRecordCubit.value({ required super.record, required T Function(List data) decodeState, }) : super.value( - initialStateFunction: _makeInitialStateFunction(decodeState), - stateFunction: _makeStateFunction(decodeState), - ); + initialStateFunction: _makeInitialStateFunction(decodeState), + stateFunction: _makeStateFunction(decodeState), + watchFunction: _makeWatchFunction()); static InitialStateFunction _makeInitialStateFunction( T Function(List data) decodeState) => @@ -155,6 +163,11 @@ class DefaultDHTRecordCubit extends DHTRecordCubit { return null; }; + static WatchFunction _makeWatchFunction() => (record) async { + final defaultSubkey = record.subkeyOrDefault(-1); + await record.watch(subkeys: [ValueSubkeyRange.single(defaultSubkey)]); + }; + Future refreshDefault() async { final defaultSubkey = _record.subkeyOrDefault(-1); await refresh([ValueSubkeyRange(low: defaultSubkey, high: defaultSubkey)]); diff --git a/packages/veilid_support/lib/dht_support/src/dht_record_pool.dart b/packages/veilid_support/lib/dht_support/src/dht_record_pool.dart index bdc4946..bb835d1 100644 --- a/packages/veilid_support/lib/dht_support/src/dht_record_pool.dart +++ b/packages/veilid_support/lib/dht_support/src/dht_record_pool.dart @@ -70,7 +70,6 @@ class SharedDHTRecordData { KeyPair? defaultWriter; VeilidRoutingContext defaultRoutingContext; Map subkeySeqCache = {}; - bool inWatchStateUpdate = false; bool needsWatchStateUpdate = false; } @@ -233,42 +232,45 @@ class DHTRecordPool with TableDBBacked { } Future delete(TypedKey recordKey) async { - // Collect all dependencies (including the record itself) - final allDeps = []; - final currentDeps = [recordKey]; - while (currentDeps.isNotEmpty) { - final nextDep = currentDeps.removeLast(); - - // Remove this child from its parent - await _removeDependencyInner(nextDep); - - allDeps.add(nextDep); - final childDeps = - _state.childrenByParent[nextDep.toJson()]?.toList() ?? []; - currentDeps.addAll(childDeps); - } - - // Delete all dependent records in parallel (including the record itself) - final allDeleteFutures = >[]; - final allCloseFutures = >[]; final allDeletedRecords = {}; - for (final dep in allDeps) { - // If record is opened, close it first - final openinfo = _opened[dep]; - if (openinfo != null) { - for (final rec in openinfo.records) { - allCloseFutures.add(rec.close()); - allDeletedRecords.add(rec); - } + final allDeletedRecordKeys = []; + + await _mutex.protect(() async { + // Collect all dependencies (including the record itself) + final allDeps = []; + final currentDeps = [recordKey]; + while (currentDeps.isNotEmpty) { + final nextDep = currentDeps.removeLast(); + + // Remove this child from its parent + await _removeDependencyInner(nextDep); + + allDeps.add(nextDep); + final childDeps = + _state.childrenByParent[nextDep.toJson()]?.toList() ?? []; + currentDeps.addAll(childDeps); } - // Then delete - allDeleteFutures.add(_routingContext.deleteDHTRecord(dep)); - } - await Future.wait(allCloseFutures); - await Future.wait(allDeleteFutures); + + // Delete all dependent records in parallel (including the record itself) + for (final dep in allDeps) { + // If record is opened, close it first + final openinfo = _opened[dep]; + if (openinfo != null) { + for (final rec in openinfo.records) { + allDeletedRecords.add(rec); + } + } + // Then delete + allDeletedRecordKeys.add(dep); + } + }); + + await Future.wait(allDeletedRecords.map((r) => r.close())); for (final deletedRecord in allDeletedRecords) { deletedRecord._markDeleted(); } + await Future.wait( + allDeletedRecordKeys.map(_routingContext.deleteDHTRecord)); } void _validateParent(TypedKey? parent, TypedKey child) { @@ -454,14 +456,27 @@ class DHTRecordPool with TableDBBacked { return _state.parentByChild[childJson]; } + /// Handle the DHT record updates coming from internal to this app + void processLocalValueChange(TypedKey key, Uint8List data, int subkey) { + // Change + for (final kv in _opened.entries) { + if (kv.key == key) { + for (final rec in kv.value.records) { + rec._addLocalValueChange(data, subkey); + } + break; + } + } + } + /// Handle the DHT record updates coming from Veilid - void processUpdateValueChange(VeilidUpdateValueChange updateValueChange) { + void processRemoteValueChange(VeilidUpdateValueChange updateValueChange) { if (updateValueChange.subkeys.isNotEmpty) { // Change for (final kv in _opened.entries) { if (kv.key == updateValueChange.key) { for (final rec in kv.value.records) { - rec.addRemoteValueChange(updateValueChange); + rec._addRemoteValueChange(updateValueChange); } break; } @@ -569,61 +584,56 @@ class DHTRecordPool with TableDBBacked { inTick = true; try { // See if any opened records need watch state changes - final unord = >[]; + final unord = Function()>[]; - for (final kv in _opened.entries) { - final openedRecordKey = kv.key; - final openedRecordInfo = kv.value; - final dhtctx = openedRecordInfo.shared.defaultRoutingContext; + await _mutex.protect(() async { + for (final kv in _opened.entries) { + final openedRecordKey = kv.key; + final openedRecordInfo = kv.value; + final dhtctx = openedRecordInfo.shared.defaultRoutingContext; - // Check if already updating - if (openedRecordInfo.shared.inWatchStateUpdate) { - continue; - } + if (openedRecordInfo.shared.needsWatchStateUpdate) { + final watchState = + _collectUnionWatchState(openedRecordInfo.records); - if (openedRecordInfo.shared.needsWatchStateUpdate) { - openedRecordInfo.shared.inWatchStateUpdate = true; + // Apply watch changes for record + if (watchState == null) { + unord.add(() async { + // Record needs watch cancel + try { + await dhtctx.cancelDHTWatch(openedRecordKey); + openedRecordInfo.shared.needsWatchStateUpdate = false; + } on VeilidAPIException { + // Failed to cancel DHT watch, try again next tick + } + }); + } else { + unord.add(() async { + // Record needs new watch + try { + final realExpiration = await dhtctx.watchDHTValues( + openedRecordKey, + subkeys: watchState.subkeys?.toList(), + count: watchState.count, + expiration: watchState.expiration); - final watchState = _collectUnionWatchState(openedRecordInfo.records); - - // Apply watch changes for record - if (watchState == null) { - unord.add(() async { - // Record needs watch cancel - try { - final done = await dhtctx.cancelDHTWatch(openedRecordKey); - assert(done, - 'should always be done when cancelling whole subkey range'); - openedRecordInfo.shared.needsWatchStateUpdate = false; - } on VeilidAPIException { - // Failed to cancel DHT watch, try again next tick - } - openedRecordInfo.shared.inWatchStateUpdate = false; - }()); - } else { - unord.add(() async { - // Record needs new watch - try { - final realExpiration = await dhtctx.watchDHTValues( - openedRecordKey, - subkeys: watchState.subkeys?.toList(), - count: watchState.count, - expiration: watchState.expiration); - openedRecordInfo.shared.needsWatchStateUpdate = false; - - // Update watch states with real expiration - _updateWatchExpirations( - openedRecordInfo.records, realExpiration); - } on VeilidAPIException { - // Failed to cancel DHT watch, try again next tick - } - openedRecordInfo.shared.inWatchStateUpdate = false; - }()); + // Update watch states with real expiration + if (realExpiration.value != BigInt.zero) { + openedRecordInfo.shared.needsWatchStateUpdate = false; + _updateWatchExpirations( + openedRecordInfo.records, realExpiration); + } + } on VeilidAPIException { + // Failed to cancel DHT watch, try again next tick + } + }); + } } } - } + }); - await unord.wait; + // Process all watch changes + await unord.map((f) => f()).wait; } finally { inTick = false; } diff --git a/packages/veilid_support/lib/dht_support/src/dht_short_array.dart b/packages/veilid_support/lib/dht_support/src/dht_short_array.dart index b54221a..09bd2d7 100644 --- a/packages/veilid_support/lib/dht_support/src/dht_short_array.dart +++ b/packages/veilid_support/lib/dht_support/src/dht_short_array.dart @@ -30,7 +30,12 @@ class _DHTShortArrayCache { } } +/////////////////////////////////////////////////////////////////////// + class DHTShortArray { + //////////////////////////////////////////////////////////////// + // Constructors + DHTShortArray._({required DHTRecord headRecord}) : _headRecord = headRecord, _head = _DHTShortArrayCache(), @@ -53,22 +58,6 @@ class DHTShortArray { _stride = stride; } - static const maxElements = 256; - - // Head DHT record - final DHTRecord _headRecord; - late final int _stride; - - // Cached representation refreshed from head record - _DHTShortArrayCache _head; - - // Subscription to head and linked record internal changes - final Map> _subscriptions; - // Stream of external changes - StreamController? _watchController; - // Watch mutex to ensure we keep the representation valid - final Mutex _listenMutex; - // Create a DHTShortArray // if smplWriter is specified, uses a SMPL schema with a single writer // rather than the key owner @@ -163,148 +152,11 @@ class DHTShortArray { crypto: crypto, ); + //////////////////////////////////////////////////////////////////////////// + // Public API + DHTRecord get record => _headRecord; - - //////////////////////////////////////////////////////////////// - - /// Serialize and write out the current head record, possibly updating it - /// if a newer copy is available online. Returns true if the write was - /// successful - Future _tryWriteHead() async { - final head = _head.toProto(); - final headBuffer = head.writeToBuffer(); - - final existingData = await _headRecord.tryWriteBytes(headBuffer); - if (existingData != null) { - // Head write failed, incorporate update - await _newHead(proto.DHTShortArray.fromBuffer(existingData)); - return false; - } - - return true; - } - - /// Validate the head from the DHT is properly formatted - /// and calculate the free list from it while we're here - List _validateHeadCacheData( - List> linkedKeys, List index) { - // Ensure nothing is duplicated in the linked keys set - final newKeys = linkedKeys.toSet(); - assert(newKeys.length <= (maxElements + (_stride - 1)) ~/ _stride, - 'too many keys'); - assert(newKeys.length == linkedKeys.length, 'duplicated linked keys'); - final newIndex = index.toSet(); - assert(newIndex.length <= maxElements, 'too many indexes'); - assert(newIndex.length == index.length, 'duplicated index locations'); - // Ensure all the index keys fit into the existing records - final indexCapacity = (linkedKeys.length + 1) * _stride; - int? maxIndex; - for (final idx in newIndex) { - assert(idx >= 0 || idx < indexCapacity, 'index out of range'); - if (maxIndex == null || idx > maxIndex) { - maxIndex = idx; - } - } - final free = []; - if (maxIndex != null) { - for (var i = 0; i < maxIndex; i++) { - if (!newIndex.contains(i)) { - free.add(i); - } - } - } - return free; - } - - /// Open a linked record for reading or writing, same as the head record - Future _openLinkedRecord(TypedKey recordKey) async { - final writer = _headRecord.writer; - return (writer != null) - ? await DHTRecordPool.instance.openWrite( - recordKey, - writer, - parent: _headRecord.key, - routingContext: _headRecord.routingContext, - ) - : await DHTRecordPool.instance.openRead( - recordKey, - parent: _headRecord.key, - routingContext: _headRecord.routingContext, - ); - } - - /// Validate a new head record - Future _newHead(proto.DHTShortArray head) async { - // Get the set of new linked keys and validate it - final linkedKeys = head.keys.map((p) => p.toVeilid()).toList(); - final index = head.index; - final free = _validateHeadCacheData(linkedKeys, index); - - // See which records are actually new - final oldRecords = Map.fromEntries( - _head.linkedRecords.map((lr) => MapEntry(lr.key, lr))); - final newRecords = {}; - final sameRecords = {}; - try { - for (var n = 0; n < linkedKeys.length; n++) { - final newKey = linkedKeys[n]; - final oldRecord = oldRecords[newKey]; - if (oldRecord == null) { - // Open the new record - final newRecord = await _openLinkedRecord(newKey); - newRecords[newKey] = newRecord; - } else { - sameRecords[newKey] = oldRecord; - } - } - } on Exception catch (_) { - // On any exception close the records we have opened - await Future.wait(newRecords.entries.map((e) => e.value.close())); - rethrow; - } - - // From this point forward we should not throw an exception or everything - // is possibly invalid. Just pass the exception up it happens and the caller - // will have to delete this short array and reopen it if it can - await Future.wait(oldRecords.entries - .where((e) => !sameRecords.containsKey(e.key)) - .map((e) => e.value.close())); - - // Figure out which indices are free - - // Make the new head cache - _head = _DHTShortArrayCache() - ..linkedRecords.addAll( - linkedKeys.map((key) => (sameRecords[key] ?? newRecords[key])!)) - ..index.addAll(index) - ..free.addAll(free); - - // Update watch if we have one in case linked records have been added - if (_watchController != null) { - await _watchAllRecords(); - } - } - - /// Pull the latest or updated copy of the head record from the network - Future _refreshHead( - {bool forceRefresh = true, bool onlyUpdates = false}) async { - // Get an updated head record copy if one exists - final head = await _headRecord.getProtobuf(proto.DHTShortArray.fromBuffer, - subkey: 0, forceRefresh: forceRefresh, onlyUpdates: onlyUpdates); - if (head == null) { - if (onlyUpdates) { - // No update - return false; - } - throw StateError('head missing during refresh'); - } - - await _newHead(head); - - return true; - } - - //////////////////////////////////////////////////////////////// + int get length => _head.index.length; Future close() async { await _watchController?.close(); @@ -345,71 +197,6 @@ class DHTShortArray { } } - DHTRecord? _getLinkedRecord(int recordNumber) { - if (recordNumber == 0) { - return _headRecord; - } - recordNumber--; - if (recordNumber >= _head.linkedRecords.length) { - return null; - } - return _head.linkedRecords[recordNumber]; - } - - Future _getOrCreateLinkedRecord(int recordNumber) async { - if (recordNumber == 0) { - return _headRecord; - } - final pool = DHTRecordPool.instance; - recordNumber--; - while (recordNumber >= _head.linkedRecords.length) { - // Linked records must use SMPL schema so writer can be specified - // Use the same writer as the head record - final smplWriter = _headRecord.writer!; - final parent = pool.getParentRecordKey(_headRecord.key); - final routingContext = _headRecord.routingContext; - final crypto = _headRecord.crypto; - - final schema = DHTSchema.smpl( - oCnt: 0, - members: [DHTSchemaMember(mKey: smplWriter.key, mCnt: _stride)]); - final dhtCreateRecord = await pool.create( - parent: parent, - routingContext: routingContext, - schema: schema, - crypto: crypto, - writer: smplWriter); - // Reopen with SMPL writer - await dhtCreateRecord.close(); - final dhtRecord = await pool.openWrite(dhtCreateRecord.key, smplWriter, - parent: parent, routingContext: routingContext, crypto: crypto); - - // Add to linked records - _head.linkedRecords.add(dhtRecord); - if (!await _tryWriteHead()) { - await _refreshHead(); - } - } - return _head.linkedRecords[recordNumber]; - } - - int _emptyIndex() { - if (_head.free.isNotEmpty) { - return _head.free.removeLast(); - } - if (_head.index.length == maxElements) { - throw StateError('too many elements'); - } - return _head.index.length; - } - - void _freeIndex(int idx) { - _head.free.add(idx); - // xxx: free list optimization here? - } - - int get length => _head.index.length; - Future getItem(int pos, {bool forceRefresh = false}) async { await _refreshHead(forceRefresh: forceRefresh, onlyUpdates: true); @@ -679,6 +466,209 @@ class DHTShortArray { ) => eventualUpdateItem(pos, protobufUpdate(fromBuffer, update)); + //////////////////////////////////////////////////////////////// + // Internal Operations + + DHTRecord? _getLinkedRecord(int recordNumber) { + if (recordNumber == 0) { + return _headRecord; + } + recordNumber--; + if (recordNumber >= _head.linkedRecords.length) { + return null; + } + return _head.linkedRecords[recordNumber]; + } + + Future _getOrCreateLinkedRecord(int recordNumber) async { + if (recordNumber == 0) { + return _headRecord; + } + final pool = DHTRecordPool.instance; + recordNumber--; + while (recordNumber >= _head.linkedRecords.length) { + // Linked records must use SMPL schema so writer can be specified + // Use the same writer as the head record + final smplWriter = _headRecord.writer!; + final parent = pool.getParentRecordKey(_headRecord.key); + final routingContext = _headRecord.routingContext; + final crypto = _headRecord.crypto; + + final schema = DHTSchema.smpl( + oCnt: 0, + members: [DHTSchemaMember(mKey: smplWriter.key, mCnt: _stride)]); + final dhtCreateRecord = await pool.create( + parent: parent, + routingContext: routingContext, + schema: schema, + crypto: crypto, + writer: smplWriter); + // Reopen with SMPL writer + await dhtCreateRecord.close(); + final dhtRecord = await pool.openWrite(dhtCreateRecord.key, smplWriter, + parent: parent, routingContext: routingContext, crypto: crypto); + + // Add to linked records + _head.linkedRecords.add(dhtRecord); + if (!await _tryWriteHead()) { + await _refreshHead(); + } + } + return _head.linkedRecords[recordNumber]; + } + + int _emptyIndex() { + if (_head.free.isNotEmpty) { + return _head.free.removeLast(); + } + if (_head.index.length == maxElements) { + throw StateError('too many elements'); + } + return _head.index.length; + } + + void _freeIndex(int idx) { + _head.free.add(idx); + // xxx: free list optimization here? + } + + /// Serialize and write out the current head record, possibly updating it + /// if a newer copy is available online. Returns true if the write was + /// successful + Future _tryWriteHead() async { + final head = _head.toProto(); + final headBuffer = head.writeToBuffer(); + + final existingData = await _headRecord.tryWriteBytes(headBuffer); + if (existingData != null) { + // Head write failed, incorporate update + await _newHead(proto.DHTShortArray.fromBuffer(existingData)); + return false; + } + + return true; + } + + /// Validate the head from the DHT is properly formatted + /// and calculate the free list from it while we're here + List _validateHeadCacheData( + List> linkedKeys, List index) { + // Ensure nothing is duplicated in the linked keys set + final newKeys = linkedKeys.toSet(); + assert(newKeys.length <= (maxElements + (_stride - 1)) ~/ _stride, + 'too many keys'); + assert(newKeys.length == linkedKeys.length, 'duplicated linked keys'); + final newIndex = index.toSet(); + assert(newIndex.length <= maxElements, 'too many indexes'); + assert(newIndex.length == index.length, 'duplicated index locations'); + // Ensure all the index keys fit into the existing records + final indexCapacity = (linkedKeys.length + 1) * _stride; + int? maxIndex; + for (final idx in newIndex) { + assert(idx >= 0 || idx < indexCapacity, 'index out of range'); + if (maxIndex == null || idx > maxIndex) { + maxIndex = idx; + } + } + final free = []; + if (maxIndex != null) { + for (var i = 0; i < maxIndex; i++) { + if (!newIndex.contains(i)) { + free.add(i); + } + } + } + return free; + } + + /// Open a linked record for reading or writing, same as the head record + Future _openLinkedRecord(TypedKey recordKey) async { + final writer = _headRecord.writer; + return (writer != null) + ? await DHTRecordPool.instance.openWrite( + recordKey, + writer, + parent: _headRecord.key, + routingContext: _headRecord.routingContext, + ) + : await DHTRecordPool.instance.openRead( + recordKey, + parent: _headRecord.key, + routingContext: _headRecord.routingContext, + ); + } + + /// Validate a new head record + Future _newHead(proto.DHTShortArray head) async { + // Get the set of new linked keys and validate it + final linkedKeys = head.keys.map((p) => p.toVeilid()).toList(); + final index = head.index; + final free = _validateHeadCacheData(linkedKeys, index); + + // See which records are actually new + final oldRecords = Map.fromEntries( + _head.linkedRecords.map((lr) => MapEntry(lr.key, lr))); + final newRecords = {}; + final sameRecords = {}; + try { + for (var n = 0; n < linkedKeys.length; n++) { + final newKey = linkedKeys[n]; + final oldRecord = oldRecords[newKey]; + if (oldRecord == null) { + // Open the new record + final newRecord = await _openLinkedRecord(newKey); + newRecords[newKey] = newRecord; + } else { + sameRecords[newKey] = oldRecord; + } + } + } on Exception catch (_) { + // On any exception close the records we have opened + await Future.wait(newRecords.entries.map((e) => e.value.close())); + rethrow; + } + + // From this point forward we should not throw an exception or everything + // is possibly invalid. Just pass the exception up it happens and the caller + // will have to delete this short array and reopen it if it can + await Future.wait(oldRecords.entries + .where((e) => !sameRecords.containsKey(e.key)) + .map((e) => e.value.close())); + + // Figure out which indices are free + + // Make the new head cache + _head = _DHTShortArrayCache() + ..linkedRecords.addAll( + linkedKeys.map((key) => (sameRecords[key] ?? newRecords[key])!)) + ..index.addAll(index) + ..free.addAll(free); + + // Update watch if we have one in case linked records have been added + if (_watchController != null) { + await _watchAllRecords(); + } + } + + /// Pull the latest or updated copy of the head record from the network + Future _refreshHead( + {bool forceRefresh = true, bool onlyUpdates = false}) async { + // Get an updated head record copy if one exists + final head = await _headRecord.getProtobuf(proto.DHTShortArray.fromBuffer, + subkey: 0, forceRefresh: forceRefresh, onlyUpdates: onlyUpdates); + if (head == null) { + if (onlyUpdates) { + // No update + return false; + } + throw StateError('head missing during refresh'); + } + + await _newHead(head); + + return true; + } + // Watch head and all linked records Future _watchAllRecords() async { // This will update any existing watches if necessary @@ -719,7 +709,7 @@ class DHTShortArray { // Called when a head or linked record changes Future _onUpdateRecord( - DHTRecord record, Uint8List data, List subkeys) async { + DHTRecord record, Uint8List? data, List subkeys) async { // If head record subkey zero changes, then the layout // of the dhtshortarray has changed var updateHead = false; @@ -772,4 +762,23 @@ class DHTShortArray { // Return subscription return _watchController!.stream.listen((_) => onChanged()); }); + + //////////////////////////////////////////////////////////////// + // Fields + + static const maxElements = 256; + + // Head DHT record + final DHTRecord _headRecord; + late final int _stride; + + // Cached representation refreshed from head record + _DHTShortArrayCache _head; + + // Subscription to head and linked record internal changes + final Map> _subscriptions; + // Stream of external changes + StreamController? _watchController; + // Watch mutex to ensure we keep the representation valid + final Mutex _listenMutex; } diff --git a/packages/veilid_support/lib/dht_support/src/dht_short_array_cubit.dart b/packages/veilid_support/lib/dht_support/src/dht_short_array_cubit.dart index afccbde..cbcd54d 100644 --- a/packages/veilid_support/lib/dht_support/src/dht_short_array_cubit.dart +++ b/packages/veilid_support/lib/dht_support/src/dht_short_array_cubit.dart @@ -66,18 +66,20 @@ class DHTShortArrayCubit extends Cubit>> { // Keep updating until we don't want to update any more // Because this is async, we could get an update while we're // still processing the last one - do { - _wantsUpdate = false; - try { - final initialState = await _getElements(); - emit(AsyncValue.data(initialState)); - } on Exception catch (e) { - emit(AsyncValue.error(e)); - } - } while (_wantsUpdate); - - // Note that this update future has finished - _isUpdating = false; + try { + do { + _wantsUpdate = false; + try { + final initialState = await _getElements(); + emit(AsyncValue.data(initialState)); + } on Exception catch (e) { + emit(AsyncValue.error(e)); + } + } while (_wantsUpdate); + } finally { + // Note that this update future has finished + _isUpdating = false; + } }); } From 43b01c75555e84aa29fbd3c0a9942712702dc6ab Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Mon, 26 Feb 2024 23:34:17 -0500 Subject: [PATCH 47/68] refactor bloc tools to its own package --- lib/chat/cubits/active_chat_cubit.dart | 2 +- ..._conversation_messages_bloc_map_cubit.dart | 2 +- .../active_conversations_bloc_map_cubit.dart | 2 +- lib/chat_list/cubits/chat_list_cubit.dart | 2 + .../chat_single_contact_list_widget.dart | 1 - .../cubits/waiting_invitation_cubit.dart | 1 + .../waiting_invitations_bloc_map_cubit.dart | 2 +- .../views/contact_invitation_display.dart | 1 + lib/contacts/cubits/conversation_cubit.dart | 2 +- lib/contacts/views/contact_list_widget.dart | 1 - lib/settings/preferences_cubit.dart | 3 +- lib/tools/tools.dart | 7 -- .../cubit/connection_state_cubit.dart | 3 +- packages/async_tools/pubspec.yaml | 8 +- packages/bloc_tools/.gitignore | 7 ++ packages/bloc_tools/analysis_options.yaml | 15 +++ .../example/bloc_tools_example.dart | 6 ++ packages/bloc_tools/lib/bloc_tools.dart | 11 ++ .../lib/src}/async_transformer_cubit.dart | 0 .../bloc_tools/lib/src/bloc_busy_wrapper.dart | 54 ++++++++++ .../bloc_tools/lib/src}/bloc_map_cubit.dart | 0 .../lib/src/bloc_tools_extension.dart | 0 .../bloc_tools/lib/src}/future_cubit.dart | 0 .../bloc_tools/lib/src}/state_follower.dart | 0 .../lib/src}/stream_wrapper_cubit.dart | 0 .../lib/src}/transformer_cubit.dart | 0 packages/bloc_tools/pubspec.yaml | 24 +++++ packages/bloc_tools/test/bloc_tools_test.dart | 16 +++ packages/mutex/pubspec.yaml | 6 +- .../src/dht_short_array_cubit.dart | 35 +++--- packages/veilid_support/pubspec.lock | 77 +++++++------ packages/veilid_support/pubspec.yaml | 18 ++-- pubspec.lock | 102 +++++++++--------- pubspec.yaml | 22 ++-- 34 files changed, 284 insertions(+), 146 deletions(-) create mode 100644 packages/bloc_tools/.gitignore create mode 100644 packages/bloc_tools/analysis_options.yaml create mode 100644 packages/bloc_tools/example/bloc_tools_example.dart create mode 100644 packages/bloc_tools/lib/bloc_tools.dart rename {lib/tools => packages/bloc_tools/lib/src}/async_transformer_cubit.dart (100%) create mode 100644 packages/bloc_tools/lib/src/bloc_busy_wrapper.dart rename {lib/tools => packages/bloc_tools/lib/src}/bloc_map_cubit.dart (100%) rename lib/tools/bloc_tools.dart => packages/bloc_tools/lib/src/bloc_tools_extension.dart (100%) rename {lib/tools => packages/bloc_tools/lib/src}/future_cubit.dart (100%) rename {lib/tools => packages/bloc_tools/lib/src}/state_follower.dart (100%) rename {lib/tools => packages/bloc_tools/lib/src}/stream_wrapper_cubit.dart (100%) rename {lib/tools => packages/bloc_tools/lib/src}/transformer_cubit.dart (100%) create mode 100644 packages/bloc_tools/pubspec.yaml create mode 100644 packages/bloc_tools/test/bloc_tools_test.dart diff --git a/lib/chat/cubits/active_chat_cubit.dart b/lib/chat/cubits/active_chat_cubit.dart index fa88d56..5c7119d 100644 --- a/lib/chat/cubits/active_chat_cubit.dart +++ b/lib/chat/cubits/active_chat_cubit.dart @@ -1,7 +1,7 @@ +import 'package:bloc_tools/bloc_tools.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:veilid_support/veilid_support.dart'; -import '../../tools/tools.dart'; class ActiveChatCubit extends Cubit with BlocTools { ActiveChatCubit(super.initialState); diff --git a/lib/chat_list/cubits/active_conversation_messages_bloc_map_cubit.dart b/lib/chat_list/cubits/active_conversation_messages_bloc_map_cubit.dart index a906bfc..3fd47f2 100644 --- a/lib/chat_list/cubits/active_conversation_messages_bloc_map_cubit.dart +++ b/lib/chat_list/cubits/active_conversation_messages_bloc_map_cubit.dart @@ -1,13 +1,13 @@ import 'dart:async'; import 'package:async_tools/async_tools.dart'; +import 'package:bloc_tools/bloc_tools.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:veilid_support/veilid_support.dart'; import '../../account_manager/account_manager.dart'; import '../../chat/chat.dart'; import '../../proto/proto.dart' as proto; -import '../../tools/tools.dart'; import 'active_conversations_bloc_map_cubit.dart'; // Map of remoteConversationRecordKey to MessagesCubit diff --git a/lib/chat_list/cubits/active_conversations_bloc_map_cubit.dart b/lib/chat_list/cubits/active_conversations_bloc_map_cubit.dart index 78bf8ff..0c32523 100644 --- a/lib/chat_list/cubits/active_conversations_bloc_map_cubit.dart +++ b/lib/chat_list/cubits/active_conversations_bloc_map_cubit.dart @@ -1,4 +1,5 @@ import 'package:async_tools/async_tools.dart'; +import 'package:bloc_tools/bloc_tools.dart'; import 'package:equatable/equatable.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:meta/meta.dart'; @@ -7,7 +8,6 @@ import 'package:veilid_support/veilid_support.dart'; import '../../account_manager/account_manager.dart'; import '../../contacts/contacts.dart'; import '../../proto/proto.dart' as proto; -import '../../tools/tools.dart'; @immutable class ActiveConversationState extends Equatable { diff --git a/lib/chat_list/cubits/chat_list_cubit.dart b/lib/chat_list/cubits/chat_list_cubit.dart index f0b85ee..606c5b8 100644 --- a/lib/chat_list/cubits/chat_list_cubit.dart +++ b/lib/chat_list/cubits/chat_list_cubit.dart @@ -1,5 +1,7 @@ import 'dart:async'; +import 'package:bloc/bloc.dart'; +import 'package:bloc_tools/bloc_tools.dart'; import 'package:veilid_support/veilid_support.dart'; import '../../account_manager/account_manager.dart'; diff --git a/lib/chat_list/views/chat_single_contact_list_widget.dart b/lib/chat_list/views/chat_single_contact_list_widget.dart index 4a31e2d..4331187 100644 --- a/lib/chat_list/views/chat_single_contact_list_widget.dart +++ b/lib/chat_list/views/chat_single_contact_list_widget.dart @@ -37,7 +37,6 @@ class ChatSingleContactListWidget extends StatelessWidget { child: (chatList.isEmpty) ? const EmptyChatListWidget() : SearchableList( - autoFocusOnSearch: false, initialList: chatList.toList(), builder: (l, i, c) { final contact = diff --git a/lib/contact_invitation/cubits/waiting_invitation_cubit.dart b/lib/contact_invitation/cubits/waiting_invitation_cubit.dart index 0c618c0..04342e0 100644 --- a/lib/contact_invitation/cubits/waiting_invitation_cubit.dart +++ b/lib/contact_invitation/cubits/waiting_invitation_cubit.dart @@ -1,6 +1,7 @@ import 'dart:typed_data'; import 'package:async_tools/async_tools.dart'; +import 'package:bloc_tools/bloc_tools.dart'; import 'package:equatable/equatable.dart'; import 'package:meta/meta.dart'; import 'package:veilid_support/veilid_support.dart'; diff --git a/lib/contact_invitation/cubits/waiting_invitations_bloc_map_cubit.dart b/lib/contact_invitation/cubits/waiting_invitations_bloc_map_cubit.dart index 15f0649..3762d28 100644 --- a/lib/contact_invitation/cubits/waiting_invitations_bloc_map_cubit.dart +++ b/lib/contact_invitation/cubits/waiting_invitations_bloc_map_cubit.dart @@ -1,10 +1,10 @@ import 'package:async_tools/async_tools.dart'; +import 'package:bloc_tools/bloc_tools.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:veilid_support/veilid_support.dart'; import '../../account_manager/account_manager.dart'; import '../../proto/proto.dart' as proto; -import '../../tools/tools.dart'; import 'cubits.dart'; typedef WaitingInvitationsBlocMapState diff --git a/lib/contact_invitation/views/contact_invitation_display.dart b/lib/contact_invitation/views/contact_invitation_display.dart index 4e75f57..fe2ed33 100644 --- a/lib/contact_invitation/views/contact_invitation_display.dart +++ b/lib/contact_invitation/views/contact_invitation_display.dart @@ -2,6 +2,7 @@ import 'dart:math'; import 'package:awesome_extensions/awesome_extensions.dart'; import 'package:basic_utils/basic_utils.dart'; +import 'package:bloc_tools/bloc_tools.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; diff --git a/lib/contacts/cubits/conversation_cubit.dart b/lib/contacts/cubits/conversation_cubit.dart index c927a1b..91ae098 100644 --- a/lib/contacts/cubits/conversation_cubit.dart +++ b/lib/contacts/cubits/conversation_cubit.dart @@ -64,7 +64,7 @@ class ConversationCubit extends Cubit> { // Open remote record key if it is specified final pool = DHTRecordPool.instance; final crypto = await getConversationCrypto(); - final record = await pool.openRead(_remoteConversationRecordKey!, + final record = await pool.openRead(_remoteConversationRecordKey, parent: accountRecordKey, crypto: crypto); await _setRemoteConversation(record); }); diff --git a/lib/contacts/views/contact_list_widget.dart b/lib/contacts/views/contact_list_widget.dart index 12a4d0a..10c4a64 100644 --- a/lib/contacts/views/contact_list_widget.dart +++ b/lib/contacts/views/contact_list_widget.dart @@ -35,7 +35,6 @@ class ContactListWidget extends StatelessWidget { child: (contactList.isEmpty) ? const EmptyContactListWidget() : SearchableList( - autoFocusOnSearch: false, initialList: contactList.toList(), builder: (l, i, c) => ContactItemWidget(contact: c), filter: (value) { diff --git a/lib/settings/preferences_cubit.dart b/lib/settings/preferences_cubit.dart index 5d85ce5..6cfd249 100644 --- a/lib/settings/preferences_cubit.dart +++ b/lib/settings/preferences_cubit.dart @@ -1,4 +1,5 @@ -import '../tools/tools.dart'; +import 'package:bloc_tools/bloc_tools.dart'; + import 'settings.dart'; class PreferencesCubit extends StreamWrapperCubit { diff --git a/lib/tools/tools.dart b/lib/tools/tools.dart index 4c9cf07..0457d43 100644 --- a/lib/tools/tools.dart +++ b/lib/tools/tools.dart @@ -1,19 +1,12 @@ export 'animations.dart'; -export 'async_transformer_cubit.dart'; -export 'bloc_map_cubit.dart'; -export 'bloc_tools.dart'; export 'enter_password.dart'; export 'enter_pin.dart'; -export 'future_cubit.dart'; export 'loggy.dart'; export 'phono_byte.dart'; export 'responsive.dart'; export 'scanner_error_widget.dart'; export 'shared_preferences.dart'; -export 'state_follower.dart'; export 'state_logger.dart'; export 'stream_listenable.dart'; -export 'stream_wrapper_cubit.dart'; -export 'transformer_cubit.dart'; export 'widget_helpers.dart'; export 'window_control.dart'; diff --git a/lib/veilid_processor/cubit/connection_state_cubit.dart b/lib/veilid_processor/cubit/connection_state_cubit.dart index e3ef7fa..8bf18f8 100644 --- a/lib/veilid_processor/cubit/connection_state_cubit.dart +++ b/lib/veilid_processor/cubit/connection_state_cubit.dart @@ -1,4 +1,5 @@ -import '../../tools/tools.dart'; +import 'package:bloc_tools/bloc_tools.dart'; + import '../models/models.dart'; import '../repository/processor_repository.dart'; diff --git a/packages/async_tools/pubspec.yaml b/packages/async_tools/pubspec.yaml index 34ae0a4..42d7a71 100644 --- a/packages/async_tools/pubspec.yaml +++ b/packages/async_tools/pubspec.yaml @@ -4,16 +4,16 @@ version: 1.0.0 publish_to: none environment: - sdk: ^3.2.6 + sdk: '>=3.2.0 <4.0.0' # Add regular dependencies here. dependencies: - freezed_annotation: ^2.2.0 + freezed_annotation: ^2.4.1 mutex: path: ../mutex dev_dependencies: build_runner: ^2.4.8 - freezed: ^2.3.5 + freezed: ^2.4.7 lint_hard: ^4.0.0 - test: ^1.24.0 + test: ^1.25.2 diff --git a/packages/bloc_tools/.gitignore b/packages/bloc_tools/.gitignore new file mode 100644 index 0000000..3cceda5 --- /dev/null +++ b/packages/bloc_tools/.gitignore @@ -0,0 +1,7 @@ +# https://dart.dev/guides/libraries/private-files +# Created by `dart pub` +.dart_tool/ + +# Avoid committing pubspec.lock for library packages; see +# https://dart.dev/guides/libraries/private-files#pubspeclock. +pubspec.lock diff --git a/packages/bloc_tools/analysis_options.yaml b/packages/bloc_tools/analysis_options.yaml new file mode 100644 index 0000000..e1620f7 --- /dev/null +++ b/packages/bloc_tools/analysis_options.yaml @@ -0,0 +1,15 @@ +include: package:lint_hard/all.yaml +analyzer: + errors: + invalid_annotation_target: ignore + exclude: + - '**/*.g.dart' + - '**/*.freezed.dart' + - '**/*.pb.dart' + - '**/*.pbenum.dart' + - '**/*.pbjson.dart' + - '**/*.pbserver.dart' +linter: + rules: + unawaited_futures: true + avoid_positional_boolean_parameters: false \ No newline at end of file diff --git a/packages/bloc_tools/example/bloc_tools_example.dart b/packages/bloc_tools/example/bloc_tools_example.dart new file mode 100644 index 0000000..25e6326 --- /dev/null +++ b/packages/bloc_tools/example/bloc_tools_example.dart @@ -0,0 +1,6 @@ +// import 'package:bloc_tools/bloc_tools.dart'; + +// void main() { +// var awesome = Awesome(); +// print('awesome: ${awesome.isAwesome}'); +// } diff --git a/packages/bloc_tools/lib/bloc_tools.dart b/packages/bloc_tools/lib/bloc_tools.dart new file mode 100644 index 0000000..4cc7304 --- /dev/null +++ b/packages/bloc_tools/lib/bloc_tools.dart @@ -0,0 +1,11 @@ +/// BLoC Tools +library; + +export 'src/async_transformer_cubit.dart'; +export 'src/bloc_busy_wrapper.dart'; +export 'src/bloc_map_cubit.dart'; +export 'src/bloc_tools_extension.dart'; +export 'src/future_cubit.dart'; +export 'src/state_follower.dart'; +export 'src/stream_wrapper_cubit.dart'; +export 'src/transformer_cubit.dart'; diff --git a/lib/tools/async_transformer_cubit.dart b/packages/bloc_tools/lib/src/async_transformer_cubit.dart similarity index 100% rename from lib/tools/async_transformer_cubit.dart rename to packages/bloc_tools/lib/src/async_transformer_cubit.dart diff --git a/packages/bloc_tools/lib/src/bloc_busy_wrapper.dart b/packages/bloc_tools/lib/src/bloc_busy_wrapper.dart new file mode 100644 index 0000000..a6bb2d7 --- /dev/null +++ b/packages/bloc_tools/lib/src/bloc_busy_wrapper.dart @@ -0,0 +1,54 @@ +import 'dart:async'; + +import 'package:bloc/bloc.dart'; +import 'package:equatable/equatable.dart'; +import 'package:meta/meta.dart'; +import 'package:mutex/mutex.dart'; + +@immutable +class BlocBusyState extends Equatable { + const BlocBusyState(this.state) : busy = false; + const BlocBusyState._busy(this.state) : busy = true; + final bool busy; + final S state; + + @override + List get props => [busy, state]; +} + +mixin BlocBusyWrapper on BlocBase> { + Future busy(Future Function(void Function(S) emit) closure) async => + _mutex.protect(() async { + void busyemit(S state) { + changedState = state; + } + + // Turn on busy state + emit(BlocBusyState._busy(state.state)); + + // Run the closure + final out = await closure(busyemit); + + // If the closure did one or more 'busy emits' then + // take the most recent one and emit it for real + final finalState = changedState; + if (finalState != null && finalState != state.state) { + emit(BlocBusyState._busy(finalState)); + } else { + emit(BlocBusyState._busy(state.state)); + } + + return out; + }); + + void changeState(S state) { + if (_mutex.isLocked) { + changedState = state; + } else { + emit(BlocBusyState(state)); + } + } + + final Mutex _mutex = Mutex(); + S? changedState; +} diff --git a/lib/tools/bloc_map_cubit.dart b/packages/bloc_tools/lib/src/bloc_map_cubit.dart similarity index 100% rename from lib/tools/bloc_map_cubit.dart rename to packages/bloc_tools/lib/src/bloc_map_cubit.dart diff --git a/lib/tools/bloc_tools.dart b/packages/bloc_tools/lib/src/bloc_tools_extension.dart similarity index 100% rename from lib/tools/bloc_tools.dart rename to packages/bloc_tools/lib/src/bloc_tools_extension.dart diff --git a/lib/tools/future_cubit.dart b/packages/bloc_tools/lib/src/future_cubit.dart similarity index 100% rename from lib/tools/future_cubit.dart rename to packages/bloc_tools/lib/src/future_cubit.dart diff --git a/lib/tools/state_follower.dart b/packages/bloc_tools/lib/src/state_follower.dart similarity index 100% rename from lib/tools/state_follower.dart rename to packages/bloc_tools/lib/src/state_follower.dart diff --git a/lib/tools/stream_wrapper_cubit.dart b/packages/bloc_tools/lib/src/stream_wrapper_cubit.dart similarity index 100% rename from lib/tools/stream_wrapper_cubit.dart rename to packages/bloc_tools/lib/src/stream_wrapper_cubit.dart diff --git a/lib/tools/transformer_cubit.dart b/packages/bloc_tools/lib/src/transformer_cubit.dart similarity index 100% rename from lib/tools/transformer_cubit.dart rename to packages/bloc_tools/lib/src/transformer_cubit.dart diff --git a/packages/bloc_tools/pubspec.yaml b/packages/bloc_tools/pubspec.yaml new file mode 100644 index 0000000..73bbe16 --- /dev/null +++ b/packages/bloc_tools/pubspec.yaml @@ -0,0 +1,24 @@ +name: bloc_tools +description: A starting point for Dart libraries or applications. +version: 1.0.0 +publish_to: none + +environment: + sdk: '>=3.2.0 <4.0.0' + +dependencies: + async_tools: + path: ../async_tools + bloc: ^8.1.3 + equatable: ^2.0.5 + fast_immutable_collections: ^10.1.1 + freezed_annotation: ^2.4.1 + meta: ^1.10.0 + mutex: + path: ../mutex + +dev_dependencies: + build_runner: ^2.4.8 + freezed: ^2.4.7 + lint_hard: ^4.0.0 + test: ^1.25.2 \ No newline at end of file diff --git a/packages/bloc_tools/test/bloc_tools_test.dart b/packages/bloc_tools/test/bloc_tools_test.dart new file mode 100644 index 0000000..5a81be4 --- /dev/null +++ b/packages/bloc_tools/test/bloc_tools_test.dart @@ -0,0 +1,16 @@ +// import 'package:bloc_tools/bloc_tools.dart'; +// import 'package:test/test.dart'; + +// void main() { +// group('A group of tests', () { +// final awesome = Awesome(); + +// setUp(() { +// // Additional setup goes here. +// }); + +// test('First Test', () { +// expect(awesome.isAwesome, isTrue); +// }); +// }); +// } diff --git a/packages/mutex/pubspec.yaml b/packages/mutex/pubspec.yaml index 52d86a1..30db4a2 100644 --- a/packages/mutex/pubspec.yaml +++ b/packages/mutex/pubspec.yaml @@ -4,9 +4,9 @@ version: 3.1.0 publish_to: none environment: - sdk: '>=2.15.0 <4.0.0' + sdk: '>=3.2.0 <4.0.0' dev_dependencies: lint_hard: ^4.0.0 - pana: ^0.21.37 - test: ^1.16.3 + pana: ^0.21.45 + test: ^1.25.2 diff --git a/packages/veilid_support/lib/dht_support/src/dht_short_array_cubit.dart b/packages/veilid_support/lib/dht_support/src/dht_short_array_cubit.dart index cbcd54d..79837b2 100644 --- a/packages/veilid_support/lib/dht_support/src/dht_short_array_cubit.dart +++ b/packages/veilid_support/lib/dht_support/src/dht_short_array_cubit.dart @@ -2,11 +2,13 @@ import 'dart:async'; import 'package:async_tools/async_tools.dart'; import 'package:bloc/bloc.dart'; +import 'package:bloc_tools/bloc_tools.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import '../../veilid_support.dart'; -class DHTShortArrayCubit extends Cubit>> { +class DHTShortArrayCubit extends Cubit>>> + with BlocBusyWrapper>> { DHTShortArrayCubit({ required Future Function() open, required T Function(List data) decodeElement, @@ -14,7 +16,7 @@ class DHTShortArrayCubit extends Cubit>> { _wantsUpdate = false, _isUpdating = false, _wantsCloseRecord = false, - super(const AsyncValue.loading()) { + super(const BlocBusyState(AsyncValue.loading())) { Future.delayed(Duration.zero, () async { // Open DHT record _shortArray = await open(); @@ -34,7 +36,7 @@ class DHTShortArrayCubit extends Cubit>> { _wantsUpdate = false, _isUpdating = false, _wantsCloseRecord = false, - super(const AsyncValue.loading()) { + super(const BlocBusyState(AsyncValue.loading())) { // Make initial state update _update(); Future.delayed(Duration.zero, () async { @@ -42,21 +44,24 @@ class DHTShortArrayCubit extends Cubit>> { }); } - Future refresh({bool forceRefresh = false}) async { - var out = IList(); - // xxx could be parallelized but we need to watch out for rate limits - for (var i = 0; i < _shortArray.length; i++) { - final cir = await _shortArray.getItem(i, forceRefresh: forceRefresh); - if (cir == null) { - throw Exception('Failed to get short array element'); - } - out = out.add(_decodeElement(cir)); - } - emit(AsyncValue.data(out)); - } + Future refresh({bool forceRefresh = false}) async => busy((emit) async { + var out = IList(); + // xxx could be parallelized but we need to watch out for rate limits + for (var i = 0; i < _shortArray.length; i++) { + final cir = await _shortArray.getItem(i, forceRefresh: forceRefresh); + if (cir == null) { + throw Exception('Failed to get short array element'); + } + out = out.add(_decodeElement(cir)); + } + emit(AsyncValue.data(out)); + }); void _update() { // Run at most one background update process + +xxx convert to singleFuture with onBusy that sets wantsupdate + _wantsUpdate = true; if (_isUpdating) { return; diff --git a/packages/veilid_support/pubspec.lock b/packages/veilid_support/pubspec.lock index 3ce09e2..ede8275 100644 --- a/packages/veilid_support/pubspec.lock +++ b/packages/veilid_support/pubspec.lock @@ -44,10 +44,17 @@ packages: dependency: "direct main" description: name: bloc - sha256: "3820f15f502372d979121de1f6b97bfcf1630ebff8fe1d52fb2b0bfa49be5b49" + sha256: f53a110e3b48dcd78136c10daa5d51512443cea5e1348c9d80a320095fa2db9e url: "https://pub.dev" source: hosted - version: "8.1.2" + version: "8.1.3" + bloc_tools: + dependency: "direct main" + description: + path: "../bloc_tools" + relative: true + source: path + version: "1.0.0" boolean_selector: dependency: transitive description: @@ -92,18 +99,18 @@ packages: dependency: "direct dev" description: name: build_runner - sha256: "67d591d602906ef9201caf93452495ad1812bea2074f04e25dbd7c133785821b" + sha256: "581bacf68f89ec8792f5e5a0b2c4decd1c948e97ce659dc783688c8a88fbec21" url: "https://pub.dev" source: hosted - version: "2.4.7" + version: "2.4.8" build_runner_core: dependency: transitive description: name: build_runner_core - sha256: c9e32d21dd6626b5c163d48b037ce906bbe428bc23ab77bcd77bb21e593b6185 + sha256: "4ae8ffe5ac758da294ecf1802f2aff01558d8b1b00616aa7538ea9a8a5d50799" url: "https://pub.dev" source: hosted - version: "7.2.11" + version: "7.3.0" built_collection: dependency: transitive description: @@ -116,10 +123,10 @@ packages: dependency: transitive description: name: built_value - sha256: c9aabae0718ec394e5bc3c7272e6bb0dc0b32201a08fe185ec1d8401d3e39309 + sha256: fedde275e0a6b798c3296963c5cd224e3e1b55d0e478d5b7e65e6b540f363a0e url: "https://pub.dev" source: hosted - version: "8.8.1" + version: "8.9.1" change_case: dependency: transitive description: @@ -156,10 +163,10 @@ packages: dependency: transitive description: name: code_builder - sha256: feee43a5c05e7b3199bb375a86430b8ada1b04104f2923d0e03cc01ca87b6d84 + sha256: f692079e25e7869c14132d39f223f8eec9830eb76131925143b2129c4bb01b37 url: "https://pub.dev" source: hosted - version: "4.9.0" + version: "4.10.0" collection: dependency: transitive description: @@ -212,10 +219,10 @@ packages: dependency: "direct main" description: name: fast_immutable_collections - sha256: "3eb1d7495c70598964add20e10666003fad6e855b108fe684ebcbf8ad0c8e120" + sha256: "6df5b5bb29f52644c4c653ef0ae7d26c8463f8d6551b0ac94561103ff6c5ca17" url: "https://pub.dev" source: hosted - version: "9.2.0" + version: "10.1.1" ffi: dependency: transitive description: @@ -262,10 +269,10 @@ packages: dependency: "direct dev" description: name: freezed - sha256: "6c5031daae12c7072b3a87eff98983076434b4889ef2a44384d0cae3f82372ba" + sha256: "57247f692f35f068cae297549a46a9a097100685c6780fe67177503eea5ed4e5" url: "https://pub.dev" source: hosted - version: "2.4.6" + version: "2.4.7" freezed_annotation: dependency: "direct main" description: @@ -334,10 +341,10 @@ packages: dependency: transitive description: name: js - sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3 + sha256: c1b2e9b5ea78c45e1a0788d29606ba27dc5f71f019f32ca5140f61ef071838cf url: "https://pub.dev" source: hosted - version: "0.6.7" + version: "0.7.1" json_annotation: dependency: "direct main" description: @@ -406,10 +413,10 @@ packages: dependency: transitive description: name: mime - sha256: e4ff8e8564c03f255408decd16e7899da1733852a9110a58fe6d1b817684a63e + sha256: "2e123074287cc9fd6c09de8336dae606d1ddb88d9ac47358826db698c176a1f2" url: "https://pub.dev" source: hosted - version: "1.0.4" + version: "1.0.5" mutex: dependency: "direct main" description: @@ -445,10 +452,10 @@ packages: dependency: transitive description: name: path_provider - sha256: a1aa8aaa2542a6bc57e381f132af822420216c80d4781f7aa085ca3229208aaa + sha256: b27217933eeeba8ff24845c34003b003b2b22151de3c908d0e679e8fe1aa078b url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.1.2" path_provider_android: dependency: transitive description: @@ -461,10 +468,10 @@ packages: dependency: transitive description: name: path_provider_foundation - sha256: "19314d595120f82aca0ba62787d58dde2cc6b5df7d2f0daf72489e38d1b57f2d" + sha256: "5a7999be66e000916500be4f15a3633ebceb8302719b47b9cc49ce924125350f" url: "https://pub.dev" source: hosted - version: "2.3.1" + version: "2.3.2" path_provider_linux: dependency: transitive description: @@ -477,10 +484,10 @@ packages: dependency: transitive description: name: path_provider_platform_interface - sha256: "94b1e0dd80970c1ce43d5d4e050a9918fce4f4a775e6142424c30a29a363265c" + sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.1.2" path_provider_windows: dependency: transitive description: @@ -493,18 +500,18 @@ packages: dependency: transitive description: name: platform - sha256: "0a279f0707af40c890e80b1e9df8bb761694c074ba7e1d4ab1bc4b728e200b59" + sha256: "12220bb4b65720483f8fa9450b4332347737cf8213dd2840d8b2c823e47243ec" url: "https://pub.dev" source: hosted - version: "3.1.3" + version: "3.1.4" plugin_platform_interface: dependency: transitive description: name: plugin_platform_interface - sha256: f4f88d4a900933e7267e2b353594774fc0d07fb072b47eedcd5b54e1ea3269f8 + sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" url: "https://pub.dev" source: hosted - version: "2.1.7" + version: "2.1.8" pool: dependency: transitive description: @@ -674,10 +681,10 @@ packages: dependency: "direct dev" description: name: test - sha256: "3d028996109ad5c253674c7f347822fb994a087614d6f353e6039704b4661ff2" + sha256: "7ee446762c2c50b3bd4ea96fe13ffac69919352bd3b4b17bac3f3465edc58073" url: "https://pub.dev" source: hosted - version: "1.25.0" + version: "1.25.2" test_api: dependency: transitive description: @@ -769,18 +776,18 @@ packages: dependency: transitive description: name: win32 - sha256: b0f37db61ba2f2e9b7a78a1caece0052564d1bc70668156cf3a29d676fe4e574 + sha256: "464f5674532865248444b4c3daca12bd9bf2d7c47f759ce2617986e7229494a8" url: "https://pub.dev" source: hosted - version: "5.1.1" + version: "5.2.0" xdg_directories: dependency: transitive description: name: xdg_directories - sha256: "589ada45ba9e39405c198fe34eb0f607cddb2108527e658136120892beac46d2" + sha256: faea9dee56b520b55a566385b84f2e8de55e7496104adada9962e0bd11bcff1d url: "https://pub.dev" source: hosted - version: "1.0.3" + version: "1.0.4" yaml: dependency: transitive description: @@ -790,5 +797,5 @@ packages: source: hosted version: "3.1.2" sdks: - dart: ">=3.2.6 <4.0.0" + dart: ">=3.2.0 <4.0.0" flutter: ">=3.10.6" diff --git a/packages/veilid_support/pubspec.yaml b/packages/veilid_support/pubspec.yaml index acbe5af..7d03995 100644 --- a/packages/veilid_support/pubspec.yaml +++ b/packages/veilid_support/pubspec.yaml @@ -4,29 +4,31 @@ publish_to: 'none' version: 1.0.2+0 environment: - sdk: '>=3.0.5 <4.0.0' + sdk: '>=3.2.0 <4.0.0' dependencies: async_tools: path: ../async_tools - bloc: ^8.1.2 + bloc: ^8.1.3 + bloc_tools: + path: ../bloc_tools equatable: ^2.0.5 - fast_immutable_collections: ^9.1.5 - freezed_annotation: ^2.2.0 + fast_immutable_collections: ^10.1.1 + freezed_annotation: ^2.4.1 json_annotation: ^4.8.1 loggy: ^2.0.3 meta: ^1.10.0 mutex: path: ../mutex - protobuf: ^3.0.0 + protobuf: ^3.1.0 veilid: # veilid: ^0.0.1 path: ../../../veilid/veilid-flutter dev_dependencies: - build_runner: ^2.4.6 - freezed: ^2.3.5 + build_runner: ^2.4.8 + freezed: ^2.4.7 json_serializable: ^6.7.1 lint_hard: ^4.0.0 - test: ^1.25.0 + test: ^1.25.2 diff --git a/pubspec.lock b/pubspec.lock index 284c9b7..4b6dbd1 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -92,10 +92,17 @@ packages: dependency: "direct main" description: name: bloc - sha256: "3820f15f502372d979121de1f6b97bfcf1630ebff8fe1d52fb2b0bfa49be5b49" + sha256: f53a110e3b48dcd78136c10daa5d51512443cea5e1348c9d80a320095fa2db9e url: "https://pub.dev" source: hosted - version: "8.1.2" + version: "8.1.3" + bloc_tools: + dependency: "direct main" + description: + path: "packages/bloc_tools" + relative: true + source: path + version: "1.0.0" blurry_modal_progress_hud: dependency: "direct main" description: @@ -172,10 +179,10 @@ packages: dependency: transitive description: name: built_value - sha256: a3ec2e0f967bc47f69f95009bb93db936288d61d5343b9436e378b28a2f830c6 + sha256: fedde275e0a6b798c3296963c5cd224e3e1b55d0e478d5b7e65e6b540f363a0e url: "https://pub.dev" source: hosted - version: "8.9.0" + version: "8.9.1" cached_network_image: dependency: transitive description: @@ -228,10 +235,10 @@ packages: dependency: transitive description: name: camera_platform_interface - sha256: fceb2c36038b6392317b1d5790c6ba9e6ca9f1da3031181b8bea03882bf9387a + sha256: a250314a48ea337b35909a4c9d5416a208d736dcb01d0b02c6af122be66660b0 url: "https://pub.dev" source: hosted - version: "2.7.3" + version: "2.7.4" camera_web: dependency: transitive description: @@ -268,10 +275,10 @@ packages: dependency: transitive description: name: charset - sha256: e8346cf597b6cea278d2d3a29b2d01ed8fb325aad718e70f22b0cb653cb31700 + sha256: "27802032a581e01ac565904ece8c8962564b1070690794f0072f6865958ce8b9" url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "2.0.1" checked_yaml: dependency: transitive description: @@ -392,22 +399,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.5" - fake_async: - dependency: transitive - description: - name: fake_async - sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78" - url: "https://pub.dev" - source: hosted - version: "1.3.1" fast_immutable_collections: dependency: "direct main" description: name: fast_immutable_collections - sha256: b910ccdc99bb38a2abbce07c5afb8f81d4e222a892e4d095a548b99814837b0c + sha256: "6df5b5bb29f52644c4c653ef0ae7d26c8463f8d6551b0ac94561103ff6c5ca17" url: "https://pub.dev" source: hosted - version: "9.2.1" + version: "10.1.1" ffi: dependency: transitive description: @@ -457,10 +456,10 @@ packages: dependency: "direct main" description: name: flutter_bloc - sha256: e74efb89ee6945bcbce74a5b3a5a3376b088e5f21f55c263fc38cbdc6237faae + sha256: "87325da1ac757fcc4813e6b34ed5dd61169973871fdf181d6c2109dd6935ece1" url: "https://pub.dev" source: hosted - version: "8.1.3" + version: "8.1.4" flutter_cache_manager: dependency: transitive description: @@ -574,15 +573,10 @@ packages: dependency: "direct main" description: name: flutter_svg - sha256: d39e7f95621fc84376bc0f7d504f05c3a41488c562f4a8ad410569127507402c + sha256: "7b4ca6cf3304575fe9c8ec64813c8d02ee41d2afe60bcfe0678bcb5375d596a2" url: "https://pub.dev" source: hosted - version: "2.0.9" - flutter_test: - dependency: "direct dev" - description: flutter - source: sdk - version: "0.0.0" + version: "2.0.10+1" flutter_translate: dependency: "direct main" description: @@ -704,10 +698,10 @@ packages: dependency: "direct main" description: name: hydrated_bloc - sha256: c925e49704c052a8f249226ae7603f86bfa776b910816390763b956c71d2cbaf + sha256: "00a2099680162e74b5a836b8a7f446e478520a9cae9f6032e028ad8129f4432d" url: "https://pub.dev" source: hosted - version: "9.1.3" + version: "9.1.4" icons_launcher: dependency: "direct dev" description: @@ -800,10 +794,10 @@ packages: dependency: transitive description: name: matcher - sha256: "1803e76e6653768d64ed8ff2e1e67bea3ad4b923eb5c56a295c3e634bad5960e" + sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb url: "https://pub.dev" source: hosted - version: "0.12.16" + version: "0.12.16+1" material_color_utilities: dependency: transitive description: @@ -1063,10 +1057,10 @@ packages: dependency: "direct main" description: name: qr_code_dart_scan - sha256: ddf409177eeef07f7bdf4e4929858b3acf139873da273315789ebc97bd17bec8 + sha256: b42d097e346a546fcf9ff2f5a0e39ea1315449608cfd9b2bc6513988b488a371 url: "https://pub.dev" source: hosted - version: "0.7.4" + version: "0.7.5" qr_flutter: dependency: "direct main" description: @@ -1135,10 +1129,10 @@ packages: dependency: "direct main" description: name: searchable_listview - sha256: fad781a412d1fec251c7a66e4aca49e49ab8b7104bda733b476d4b5c81891bea + sha256: "5cd3cd87e0cbd4e6685f6798a9bb4bcc170df20fb92beb662b978f5fccded634" url: "https://pub.dev" source: hosted - version: "2.10.1" + version: "2.10.2" share_plus: dependency: "direct main" description: @@ -1380,10 +1374,10 @@ packages: dependency: transitive description: name: test_api - sha256: "5c2f730018264d276c20e4f1503fd1308dfbbae39ec8ee63c5236311ac06954b" + sha256: "9955ae474176f7ac8ee4e989dadfb411a58c30415bcfb648fa04b2b8a03afa7f" url: "https://pub.dev" source: hosted - version: "0.6.1" + version: "0.7.0" timing: dependency: transitive description: @@ -1420,18 +1414,18 @@ packages: dependency: transitive description: name: url_launcher - sha256: c512655380d241a337521703af62d2c122bf7b77a46ff7dd750092aa9433499c + sha256: "0ecc004c62fd3ed36a2ffcbe0dd9700aee63bd7532d0b642a488b1ec310f492e" url: "https://pub.dev" source: hosted - version: "6.2.4" + version: "6.2.5" url_launcher_android: dependency: transitive description: name: url_launcher_android - sha256: "507dc655b1d9cb5ebc756032eb785f114e415f91557b73bf60b7e201dfedeb2f" + sha256: d4ed0711849dd8e33eb2dd69c25db0d0d3fdc37e0a62e629fe32f57a22db2745 url: "https://pub.dev" source: hosted - version: "6.2.2" + version: "6.3.0" url_launcher_ios: dependency: transitive description: @@ -1460,10 +1454,10 @@ packages: dependency: transitive description: name: url_launcher_platform_interface - sha256: a932c3a8082e118f80a475ce692fde89dc20fddb24c57360b96bc56f7035de1f + sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029" url: "https://pub.dev" source: hosted - version: "2.3.1" + version: "2.3.2" url_launcher_web: dependency: transitive description: @@ -1492,26 +1486,26 @@ packages: dependency: transitive description: name: vector_graphics - sha256: "4ac59808bbfca6da38c99f415ff2d3a5d7ca0a6b4809c71d9cf30fba5daf9752" + sha256: "32c3c684e02f9bc0afb0ae0aa653337a2fe022e8ab064bcd7ffda27a74e288e3" url: "https://pub.dev" source: hosted - version: "1.1.10+1" + version: "1.1.11+1" vector_graphics_codec: dependency: transitive description: name: vector_graphics_codec - sha256: f3247e7ab0ec77dc759263e68394990edc608fb2b480b80db8aa86ed09279e33 + sha256: c86987475f162fadff579e7320c7ddda04cd2fdeffbe1129227a85d9ac9e03da url: "https://pub.dev" source: hosted - version: "1.1.10+1" + version: "1.1.11+1" vector_graphics_compiler: dependency: transitive description: name: vector_graphics_compiler - sha256: "18489bdd8850de3dd7ca8a34e0c446f719ec63e2bab2e7a8cc66a9028dd76c5a" + sha256: "12faff3f73b1741a36ca7e31b292ddeb629af819ca9efe9953b70bd63fc8cd81" url: "https://pub.dev" source: hosted - version: "1.1.10+1" + version: "1.1.11+1" vector_math: dependency: transitive description: @@ -1618,18 +1612,18 @@ packages: dependency: "direct main" description: name: zxing2 - sha256: a042961441bd400f59595f9125ef5fca4c888daf0ea59c17f41e0e151f8a12b5 + sha256: "6cf995abd3c86f01ba882968dedffa7bc130185e382f2300239d2e857fc7912c" url: "https://pub.dev" source: hosted - version: "0.2.1" + version: "0.2.3" zxing_lib: dependency: transitive description: name: zxing_lib - sha256: "84f6ec19b04dd54bc0b25c539c7c3567a5f9e872e3feb23763df027a1f855c11" + sha256: "870a63610be3f20009ca9201f7ba2d53d7eaefa675c154b3e8c1f6fc55984d04" url: "https://pub.dev" source: hosted - version: "0.9.0" + version: "1.1.2" sdks: - dart: ">=3.2.6 <4.0.0" + dart: ">=3.2.3 <4.0.0" flutter: ">=3.16.6" diff --git a/pubspec.yaml b/pubspec.yaml index ebcbef7..68cbd1c 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -4,7 +4,7 @@ publish_to: 'none' version: 1.0.2+0 environment: - sdk: '>=3.0.5 <4.0.0' + sdk: '>=3.2.0 <4.0.0' flutter: ">=3.10.0" dependencies: @@ -16,7 +16,9 @@ dependencies: awesome_extensions: ^2.0.12 badges: ^3.1.2 basic_utils: ^5.7.0 - bloc: ^8.1.2 + bloc: ^8.1.3 + bloc_tools: + path: packages/bloc_tools blurry_modal_progress_hud: ^1.1.1 change_case: ^1.1.0 charcode: ^1.3.1 @@ -25,12 +27,12 @@ dependencies: cool_dropdown: ^2.1.0 cupertino_icons: ^1.0.6 equatable: ^2.0.5 - fast_immutable_collections: ^9.2.1 + fast_immutable_collections: ^10.1.1 fixnum: ^1.1.0 flutter: sdk: flutter flutter_animate: ^4.5.0 - flutter_bloc: ^8.1.3 + flutter_bloc: ^8.1.4 flutter_chat_types: ^3.6.2 flutter_chat_ui: ^1.6.12 flutter_form_builder: ^9.2.1 @@ -40,12 +42,12 @@ dependencies: flutter_native_splash: ^2.3.10 flutter_slidable: ^3.0.1 flutter_spinkit: ^5.2.0 - flutter_svg: ^2.0.9 + flutter_svg: ^2.0.10+1 flutter_translate: ^4.0.4 form_builder_validators: ^9.1.0 freezed_annotation: ^2.4.1 go_router: ^13.2.0 - hydrated_bloc: ^9.1.3 + hydrated_bloc: ^9.1.4 image: ^4.1.7 intl: ^0.18.1 json_annotation: ^4.8.1 @@ -62,12 +64,12 @@ dependencies: preload_page_view: ^0.2.0 protobuf: ^3.1.0 provider: ^6.1.1 - qr_code_dart_scan: ^0.7.4 + qr_code_dart_scan: ^0.7.5 qr_flutter: ^4.1.0 quickalert: ^1.0.2 radix_colors: ^1.0.4 reorderable_grid: ^1.0.10 - searchable_listview: ^2.10.1 + searchable_listview: ^2.10.2 share_plus: ^7.2.2 shared_preferences: ^2.2.2 signal_strength_indicator: ^0.4.1 @@ -83,12 +85,10 @@ dependencies: path: packages/veilid_support window_manager: ^0.3.8 xterm: ^3.5.0 - zxing2: ^0.2.1 + zxing2: ^0.2.3 dev_dependencies: build_runner: ^2.4.8 - flutter_test: - sdk: flutter freezed: ^2.4.7 icons_launcher: ^2.1.7 json_serializable: ^6.7.1 From c6f017b0d15a00b9d67dde791828a7995aebb281 Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Tue, 27 Feb 2024 12:45:58 -0500 Subject: [PATCH 48/68] busy handling --- lib/chat/cubits/messages_cubit.dart | 18 +++--- lib/chat/views/chat_component.dart | 3 +- .../active_conversations_bloc_map_cubit.dart | 11 ++-- lib/chat_list/cubits/chat_list_cubit.dart | 29 ++++----- .../chat_single_contact_item_widget.dart | 36 +++++++---- .../chat_single_contact_list_widget.dart | 3 +- .../cubits/contact_invitation_list_cubit.dart | 59 ++++++++++--------- .../waiting_invitations_bloc_map_cubit.dart | 10 ++-- .../views/contact_invitation_item_widget.dart | 11 +++- .../views/contact_invitation_list_widget.dart | 9 ++- lib/contacts/cubits/contact_list_cubit.dart | 58 +++++++++--------- lib/contacts/views/contact_item_widget.dart | 53 ++++++++++------- lib/contacts/views/contact_list_widget.dart | 11 +++- .../home_account_ready_shell.dart | 10 ++-- .../main_pager/account_page.dart | 16 +++-- lib/tools/widget_helpers.dart | 12 ++++ packages/async_tools/lib/async_tools.dart | 1 + .../lib/src/single_state_processor.dart | 3 +- .../lib/src/single_stateless_processor.dart | 47 +++++++++++++++ .../lib/src/async_transformer_cubit.dart | 2 +- .../bloc_tools/lib/src/bloc_busy_wrapper.dart | 25 +++++++- .../bloc_tools/lib/src/state_follower.dart | 2 +- .../src/dht_short_array_cubit.dart | 57 +++++++----------- 23 files changed, 307 insertions(+), 179 deletions(-) create mode 100644 packages/async_tools/lib/src/single_stateless_processor.dart diff --git a/lib/chat/cubits/messages_cubit.dart b/lib/chat/cubits/messages_cubit.dart index b1846cf..febc23e 100644 --- a/lib/chat/cubits/messages_cubit.dart +++ b/lib/chat/cubits/messages_cubit.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'package:async_tools/async_tools.dart'; +import 'package:bloc_tools/bloc_tools.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:veilid_support/veilid_support.dart'; @@ -61,9 +62,10 @@ class MessagesCubit extends Cubit>> { await super.close(); } - void updateLocalMessagesState(AsyncValue> avmessages) { + void updateLocalMessagesState( + BlocBusyState>> avmessages) { // Updated local messages from online just update the state immediately - emit(avmessages); + emit(avmessages.state); } Future _updateRemoteMessagesStateAsync(_MessageQueueEntry entry) async { @@ -97,16 +99,17 @@ class MessagesCubit extends Cubit>> { // Insert at this position if (!skip) { // Insert into dht backing array - await _localMessagesCubit!.shortArray - .tryInsertItem(pos, newMessage.writeToBuffer()); + await _localMessagesCubit!.operate((shortArray) => + shortArray.tryInsertItem(pos, newMessage.writeToBuffer())); // Insert into local copy as well for this operation localMessages = localMessages.insert(pos, newMessage); } } } - void updateRemoteMessagesState(AsyncValue> avmessages) { - final remoteMessages = avmessages.data?.value; + void updateRemoteMessagesState( + BlocBusyState>> avmessages) { + final remoteMessages = avmessages.state.data?.value; if (remoteMessages == null) { return; } @@ -171,7 +174,8 @@ class MessagesCubit extends Cubit>> { } Future addMessage({required proto.Message message}) async { - await _localMessagesCubit!.shortArray.tryAddItem(message.writeToBuffer()); + await _localMessagesCubit!.operate( + (shortArray) => shortArray.tryAddItem(message.writeToBuffer())); } Future getMessagesCrypto() async { diff --git a/lib/chat/views/chat_component.dart b/lib/chat/views/chat_component.dart index 6e16276..ab3d16f 100644 --- a/lib/chat/views/chat_component.dart +++ b/lib/chat/views/chat_component.dart @@ -48,7 +48,8 @@ class ChatComponent extends StatelessWidget { if (accountRecordInfo == null) { return debugPage('should always have an account record here'); } - final contactList = context.watch().state.data?.value; + final contactList = + context.watch().state.state.data?.value; if (contactList == null) { return debugPage('should always have a contact list here'); } diff --git a/lib/chat_list/cubits/active_conversations_bloc_map_cubit.dart b/lib/chat_list/cubits/active_conversations_bloc_map_cubit.dart index 0c32523..7f67f10 100644 --- a/lib/chat_list/cubits/active_conversations_bloc_map_cubit.dart +++ b/lib/chat_list/cubits/active_conversations_bloc_map_cubit.dart @@ -36,7 +36,9 @@ typedef ActiveConversationsBlocMapState // Automatically follows the state of a ChatListCubit. class ActiveConversationsBlocMapCubit extends BlocMapCubit, ActiveConversationCubit> - with StateFollower>, TypedKey, proto.Chat> { + with + StateFollower>>, TypedKey, + proto.Chat> { ActiveConversationsBlocMapCubit( {required ActiveAccountInfo activeAccountInfo, required ContactListCubit contactListCubit}) @@ -73,8 +75,9 @@ class ActiveConversationsBlocMapCubit extends BlocMapCubit getStateMap(AsyncValue> state) { - final stateValue = state.data?.value; + IMap getStateMap( + BlocBusyState>> state) { + final stateValue = state.state.data?.value; if (stateValue == null) { return IMap(); } @@ -88,7 +91,7 @@ class ActiveConversationsBlocMapCubit extends BlocMapCubit updateState(TypedKey key, proto.Chat value) async { - final contactList = _contactListCubit.state.data?.value; + final contactList = _contactListCubit.state.state.data?.value; if (contactList == null) { await addState(key, const AsyncValue.loading()); return; diff --git a/lib/chat_list/cubits/chat_list_cubit.dart b/lib/chat_list/cubits/chat_list_cubit.dart index 606c5b8..11eebad 100644 --- a/lib/chat_list/cubits/chat_list_cubit.dart +++ b/lib/chat_list/cubits/chat_list_cubit.dart @@ -1,7 +1,5 @@ import 'dart:async'; -import 'package:bloc/bloc.dart'; -import 'package:bloc_tools/bloc_tools.dart'; import 'package:veilid_support/veilid_support.dart'; import '../../account_manager/account_manager.dart'; @@ -44,7 +42,9 @@ class ChatListCubit extends DHTShortArrayCubit { // Add Chat to account's list // if this fails, don't keep retrying, user can try again later - if (await shortArray.tryAddItem(chat.writeToBuffer()) == false) { + final added = await operate( + (shortArray) => shortArray.tryAddItem(chat.writeToBuffer())); + if (!added) { throw Exception('Failed to add chat'); } } @@ -57,17 +57,18 @@ class ChatListCubit extends DHTShortArrayCubit { // Remove Chat from account's list // if this fails, don't keep retrying, user can try again later - - for (var i = 0; i < shortArray.length; i++) { - final cbuf = await shortArray.getItem(i); - if (cbuf == null) { - throw Exception('Failed to get chat'); + await operate((shortArray) async { + for (var i = 0; i < shortArray.length; i++) { + final cbuf = await shortArray.getItem(i); + if (cbuf == null) { + throw Exception('Failed to get chat'); + } + final c = proto.Chat.fromBuffer(cbuf); + if (c.remoteConversationKey == remoteConversationKey) { + await shortArray.tryRemoveItem(i); + return; + } } - final c = proto.Chat.fromBuffer(cbuf); - if (c.remoteConversationKey == remoteConversationKey) { - await shortArray.tryRemoveItem(i); - return; - } - } + }); } } diff --git a/lib/chat_list/views/chat_single_contact_item_widget.dart b/lib/chat_list/views/chat_single_contact_item_widget.dart index 7d64e43..7daa99c 100644 --- a/lib/chat_list/views/chat_single_contact_item_widget.dart +++ b/lib/chat_list/views/chat_single_contact_item_widget.dart @@ -10,10 +10,15 @@ import '../../theme/theme.dart'; import '../chat_list.dart'; class ChatSingleContactItemWidget extends StatelessWidget { - const ChatSingleContactItemWidget({required proto.Contact contact, super.key}) - : _contact = contact; + const ChatSingleContactItemWidget({ + required proto.Contact contact, + required bool disabled, + super.key, + }) : _contact = contact, + _disabled = disabled; final proto.Contact _contact; + final bool _disabled; @override // ignore: prefer_expression_function_bodies @@ -43,12 +48,14 @@ class ChatSingleContactItemWidget extends StatelessWidget { motion: const DrawerMotion(), children: [ SlidableAction( - onPressed: (context) async { - final chatListCubit = context.read(); - await chatListCubit.deleteChat( - remoteConversationRecordKey: - remoteConversationRecordKey); - }, + onPressed: _disabled + ? null + : (context) async { + final chatListCubit = context.read(); + await chatListCubit.deleteChat( + remoteConversationRecordKey: + remoteConversationRecordKey); + }, backgroundColor: scale.tertiaryScale.background, foregroundColor: scale.tertiaryScale.text, icon: Icons.delete, @@ -67,11 +74,14 @@ class ChatSingleContactItemWidget extends StatelessWidget { // The child of the Slidable is what the user sees when the // component is not dragged. child: ListTile( - onTap: () { - singleFuture(activeChatCubit, () async { - activeChatCubit.setActiveChat(remoteConversationRecordKey); - }); - }, + onTap: _disabled + ? null + : () { + singleFuture(activeChatCubit, () async { + activeChatCubit + .setActiveChat(remoteConversationRecordKey); + }); + }, title: Text(_contact.editedProfile.name), /// xxx show last message here diff --git a/lib/chat_list/views/chat_single_contact_list_widget.dart b/lib/chat_list/views/chat_single_contact_list_widget.dart index 4331187..5952dc4 100644 --- a/lib/chat_list/views/chat_single_contact_list_widget.dart +++ b/lib/chat_list/views/chat_single_contact_list_widget.dart @@ -45,7 +45,8 @@ class ChatSingleContactListWidget extends StatelessWidget { return const Text('...'); } return ChatSingleContactItemWidget( - contact: contact); + contact: contact, + disabled: contactListV.busy); }, filter: (value) { final lowerValue = value.toLowerCase(); diff --git a/lib/contact_invitation/cubits/contact_invitation_list_cubit.dart b/lib/contact_invitation/cubits/contact_invitation_list_cubit.dart index 49bc4a6..399fafa 100644 --- a/lib/contact_invitation/cubits/contact_invitation_list_cubit.dart +++ b/lib/contact_invitation/cubits/contact_invitation_list_cubit.dart @@ -138,9 +138,11 @@ class ContactInvitationListCubit // Add ContactInvitationRecord to account's list // if this fails, don't keep retrying, user can try again later - if (await shortArray.tryAddItem(cinvrec.writeToBuffer()) == false) { - throw Exception('Failed to add contact invitation record'); - } + await operate((shortArray) async { + if (await shortArray.tryAddItem(cinvrec.writeToBuffer()) == false) { + throw Exception('Failed to add contact invitation record'); + } + }); }); }); @@ -155,31 +157,34 @@ class ContactInvitationListCubit _activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey; // Remove ContactInvitationRecord from account's list - for (var i = 0; i < shortArray.length; i++) { - final item = await shortArray.getItemProtobuf( - proto.ContactInvitationRecord.fromBuffer, i); - if (item == null) { - throw Exception('Failed to get contact invitation record'); - } - if (item.contactRequestInbox.recordKey.toVeilid() == - contactRequestInboxRecordKey) { - await shortArray.tryRemoveItem(i); - - await (await pool.openOwned(item.contactRequestInbox.toVeilid(), - 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.openRead(item.localConversationRecordKey.toVeilid(), - parent: accountRecordKey)) - .delete(); + await operate((shortArray) async { + for (var i = 0; i < shortArray.length; i++) { + final item = await shortArray.getItemProtobuf( + proto.ContactInvitationRecord.fromBuffer, i); + if (item == null) { + throw Exception('Failed to get contact invitation record'); + } + if (item.contactRequestInbox.recordKey.toVeilid() == + contactRequestInboxRecordKey) { + await shortArray.tryRemoveItem(i); + + await (await pool.openOwned(item.contactRequestInbox.toVeilid(), + 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.openRead( + item.localConversationRecordKey.toVeilid(), + parent: accountRecordKey)) + .delete(); + } + return; } - return; } - } + }); } Future validateInvitation( @@ -205,7 +210,7 @@ class ContactInvitationListCubit // inbox with our list of extant invitations // If we're chatting to ourselves, // we are validating an invitation we have created - final isSelf = state.data!.value.indexWhere((cir) => + final isSelf = state.state.data!.value.indexWhere((cir) => cir.contactRequestInbox.recordKey.toVeilid() == contactRequestInboxKey) != -1; diff --git a/lib/contact_invitation/cubits/waiting_invitations_bloc_map_cubit.dart b/lib/contact_invitation/cubits/waiting_invitations_bloc_map_cubit.dart index 3762d28..584d2a1 100644 --- a/lib/contact_invitation/cubits/waiting_invitations_bloc_map_cubit.dart +++ b/lib/contact_invitation/cubits/waiting_invitations_bloc_map_cubit.dart @@ -16,8 +16,10 @@ typedef WaitingInvitationsBlocMapState class WaitingInvitationsBlocMapCubit extends BlocMapCubit, WaitingInvitationCubit> with - StateFollower>, - TypedKey, proto.ContactInvitationRecord> { + StateFollower< + BlocBusyState>>, + TypedKey, + proto.ContactInvitationRecord> { WaitingInvitationsBlocMapCubit( {required this.activeAccountInfo, required this.account}); @@ -37,8 +39,8 @@ class WaitingInvitationsBlocMapCubit extends BlocMapCubit getStateMap( - AsyncValue> state) { - final stateValue = state.data?.value; + BlocBusyState>> state) { + final stateValue = state.state.data?.value; if (stateValue == null) { return IMap(); } diff --git a/lib/contact_invitation/views/contact_invitation_item_widget.dart b/lib/contact_invitation/views/contact_invitation_item_widget.dart index 36ee50a..c6e96c1 100644 --- a/lib/contact_invitation/views/contact_invitation_item_widget.dart +++ b/lib/contact_invitation/views/contact_invitation_item_widget.dart @@ -9,15 +9,20 @@ import '../contact_invitation.dart'; class ContactInvitationItemWidget extends StatelessWidget { const ContactInvitationItemWidget( - {required this.contactInvitationRecord, super.key}); + {required this.contactInvitationRecord, + required this.disabled, + super.key}); final proto.ContactInvitationRecord contactInvitationRecord; + final bool disabled; @override void debugFillProperties(DiagnosticPropertiesBuilder properties) { super.debugFillProperties(properties); - properties.add(DiagnosticsProperty( - 'contactInvitationRecord', contactInvitationRecord)); + properties + ..add(DiagnosticsProperty( + 'contactInvitationRecord', contactInvitationRecord)) + ..add(DiagnosticsProperty('disabled', disabled)); } @override diff --git a/lib/contact_invitation/views/contact_invitation_list_widget.dart b/lib/contact_invitation/views/contact_invitation_list_widget.dart index e93f746..19243b7 100644 --- a/lib/contact_invitation/views/contact_invitation_list_widget.dart +++ b/lib/contact_invitation/views/contact_invitation_list_widget.dart @@ -10,10 +10,12 @@ import 'contact_invitation_item_widget.dart'; class ContactInvitationListWidget extends StatefulWidget { const ContactInvitationListWidget({ required this.contactInvitationRecordList, + required this.disabled, super.key, }); final IList contactInvitationRecordList; + final bool disabled; @override ContactInvitationListWidgetState createState() => @@ -21,8 +23,10 @@ class ContactInvitationListWidget extends StatefulWidget { @override void debugFillProperties(DiagnosticPropertiesBuilder properties) { super.debugFillProperties(properties); - properties.add(IterableProperty( - 'contactInvitationRecordList', contactInvitationRecordList)); + properties + ..add(IterableProperty( + 'contactInvitationRecordList', contactInvitationRecordList)) + ..add(DiagnosticsProperty('disabled', disabled)); } } @@ -63,6 +67,7 @@ class ContactInvitationListWidgetState return ContactInvitationItemWidget( contactInvitationRecord: widget.contactInvitationRecordList[index], + disabled: widget.disabled, key: ObjectKey(widget.contactInvitationRecordList[index])) .paddingLTRB(4, 2, 4, 2); }, diff --git a/lib/contacts/cubits/contact_list_cubit.dart b/lib/contacts/cubits/contact_list_cubit.dart index c2f5200..af66ac7 100644 --- a/lib/contacts/cubits/contact_list_cubit.dart +++ b/lib/contacts/cubits/contact_list_cubit.dart @@ -53,9 +53,11 @@ class ContactListCubit extends DHTShortArrayCubit { // Add Contact to account's list // if this fails, don't keep retrying, user can try again later - if (await shortArray.tryAddItem(contact.writeToBuffer()) == false) { - throw Exception('Failed to add contact record'); - } + await operate((shortArray) async { + if (await shortArray.tryAddItem(contact.writeToBuffer()) == false) { + throw Exception('Failed to add contact record'); + } + }); } Future deleteContact({required proto.Contact contact}) async { @@ -67,34 +69,36 @@ class ContactListCubit extends DHTShortArrayCubit { contact.remoteConversationRecordKey.toVeilid(); // Remove Contact from account's list - for (var i = 0; i < shortArray.length; i++) { - final item = - await shortArray.getItemProtobuf(proto.Contact.fromBuffer, i); - if (item == null) { - throw Exception('Failed to get contact'); + await operate((shortArray) async { + for (var i = 0; i < shortArray.length; i++) { + final item = + await shortArray.getItemProtobuf(proto.Contact.fromBuffer, i); + if (item == null) { + throw Exception('Failed to get contact'); + } + if (item.remoteConversationRecordKey == + contact.remoteConversationRecordKey) { + await shortArray.tryRemoveItem(i); + break; + } } - if (item.remoteConversationRecordKey == - contact.remoteConversationRecordKey) { - await shortArray.tryRemoveItem(i); - break; - } - } - try { - await (await pool.openRead(localConversationKey, - parent: accountRecordKey)) - .delete(); - } on Exception catch (e) { - log.debug('error removing local conversation record key: $e', e); - } - try { - if (localConversationKey != remoteConversationKey) { - await (await pool.openRead(remoteConversationKey, + try { + await (await pool.openRead(localConversationKey, parent: accountRecordKey)) .delete(); + } on Exception catch (e) { + log.debug('error removing local conversation record key: $e', e); } - } on Exception catch (e) { - log.debug('error removing remote conversation record key: $e', e); - } + try { + if (localConversationKey != remoteConversationKey) { + await (await pool.openRead(remoteConversationKey, + parent: accountRecordKey)) + .delete(); + } + } on Exception catch (e) { + log.debug('error removing remote conversation record key: $e', e); + } + }); } // diff --git a/lib/contacts/views/contact_item_widget.dart b/lib/contacts/views/contact_item_widget.dart index 864b9ab..4fc2bce 100644 --- a/lib/contacts/views/contact_item_widget.dart +++ b/lib/contacts/views/contact_item_widget.dart @@ -11,9 +11,11 @@ import '../../theme/theme.dart'; import '../contacts.dart'; class ContactItemWidget extends StatelessWidget { - const ContactItemWidget({required this.contact, super.key}); + const ContactItemWidget( + {required this.contact, required this.disabled, super.key}); final proto.Contact contact; + final bool disabled; @override // ignore: prefer_expression_function_bodies @@ -41,17 +43,22 @@ class ContactItemWidget extends StatelessWidget { motion: const DrawerMotion(), children: [ SlidableAction( - onPressed: (context) async { - final contactListCubit = context.read(); - final chatListCubit = context.read(); + onPressed: disabled || context.read().isBusy + ? null + : (context) async { + final contactListCubit = + context.read(); + final chatListCubit = context.read(); - // Remove any chats for this contact - await chatListCubit.deleteChat( - remoteConversationRecordKey: remoteConversationKey); + // Remove any chats for this contact + await chatListCubit.deleteChat( + remoteConversationRecordKey: + remoteConversationKey); - // Delete the contact itself - await contactListCubit.deleteContact(contact: contact); - }, + // Delete the contact itself + await contactListCubit.deleteContact( + contact: contact); + }, backgroundColor: scale.tertiaryScale.background, foregroundColor: scale.tertiaryScale.text, icon: Icons.delete, @@ -70,17 +77,21 @@ class ContactItemWidget extends StatelessWidget { // The child of the Slidable is what the user sees when the // component is not dragged. child: ListTile( - onTap: () async { - // Start a chat - final chatListCubit = context.read(); - await chatListCubit.getOrCreateChatSingleContact( - remoteConversationRecordKey: remoteConversationKey); - // Click over to chats - if (context.mounted) { - await MainPager.of(context)?.pageController.animateToPage(1, - duration: 250.ms, curve: Curves.easeInOut); - } - }, + onTap: disabled || context.read().isBusy + ? null + : () async { + // Start a chat + final chatListCubit = context.read(); + await chatListCubit.getOrCreateChatSingleContact( + remoteConversationRecordKey: remoteConversationKey); + // Click over to chats + if (context.mounted) { + await MainPager.of(context) + ?.pageController + .animateToPage(1, + duration: 250.ms, curve: Curves.easeInOut); + } + }, title: Text(contact.editedProfile.name), subtitle: (contact.editedProfile.pronouns.isNotEmpty) ? Text(contact.editedProfile.pronouns) diff --git a/lib/contacts/views/contact_list_widget.dart b/lib/contacts/views/contact_list_widget.dart index 10c4a64..df8cf79 100644 --- a/lib/contacts/views/contact_list_widget.dart +++ b/lib/contacts/views/contact_list_widget.dart @@ -12,13 +12,17 @@ import 'contact_item_widget.dart'; import 'empty_contact_list_widget.dart'; class ContactListWidget extends StatelessWidget { - const ContactListWidget({required this.contactList, super.key}); + const ContactListWidget( + {required this.contactList, required this.disabled, super.key}); final IList contactList; + final bool disabled; @override void debugFillProperties(DiagnosticPropertiesBuilder properties) { super.debugFillProperties(properties); - properties.add(IterableProperty('contactList', contactList)); + properties + ..add(IterableProperty('contactList', contactList)) + ..add(DiagnosticsProperty('disabled', disabled)); } @override @@ -36,7 +40,8 @@ class ContactListWidget extends StatelessWidget { ? const EmptyContactListWidget() : SearchableList( initialList: contactList.toList(), - builder: (l, i, c) => ContactItemWidget(contact: c), + builder: (l, i, c) => + ContactItemWidget(contact: c, disabled: disabled), filter: (value) { final lowerValue = value.toLowerCase(); return contactList diff --git a/lib/layout/home/home_account_ready/home_account_ready_shell.dart b/lib/layout/home/home_account_ready/home_account_ready_shell.dart index 73db595..4fee8ab 100644 --- a/lib/layout/home/home_account_ready/home_account_ready_shell.dart +++ b/lib/layout/home/home_account_ready/home_account_ready_shell.dart @@ -1,4 +1,5 @@ import 'package:async_tools/async_tools.dart'; +import 'package:bloc_tools/bloc_tools.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; @@ -75,8 +76,7 @@ class HomeAccountReadyShellState extends State { // Process all accepted or rejected invitations void _invitationStatusListener( BuildContext context, WaitingInvitationsBlocMapState state) { - _singleInvitationStatusProcessor.updateState(state, - closure: (newState) async { + _singleInvitationStatusProcessor.updateState(state, (newState) async { final contactListCubit = context.read(); final contactInvitationListCubit = context.read(); @@ -146,7 +146,8 @@ class HomeAccountReadyShellState extends State { activeAccountInfo: widget.activeAccountInfo, contactListCubit: context.read()) ..follow( - initialInputState: const AsyncValue.loading(), + initialInputState: + const BlocBusyState(AsyncValue.loading()), stream: context.read().stream)), BlocProvider( create: (context) => @@ -167,7 +168,8 @@ class HomeAccountReadyShellState extends State { activeAccountInfo: widget.activeAccountInfo, account: account) ..follow( - initialInputState: const AsyncValue.loading(), + initialInputState: + const BlocBusyState(AsyncValue.loading()), stream: context .read() .stream)) diff --git a/lib/layout/home/home_account_ready/main_pager/account_page.dart b/lib/layout/home/home_account_ready/main_pager/account_page.dart index 49a5a49..b2c8384 100644 --- a/lib/layout/home/home_account_ready/main_pager/account_page.dart +++ b/lib/layout/home/home_account_ready/main_pager/account_page.dart @@ -38,11 +38,14 @@ class AccountPageState extends State { final textTheme = theme.textTheme; final scale = theme.extension()!; + final cilState = context.watch().state; + final cilBusy = cilState.busy; final contactInvitationRecordList = - context.watch().state.data?.value ?? - const IListConst([]); - final contactList = context.watch().state.data?.value ?? - const IListConst([]); + cilState.state.data?.value ?? const IListConst([]); + + final ciState = context.watch().state; + final ciBusy = ciState.busy; + final contactList = ciState.state.data?.value ?? const IListConst([]); return SizedBox( child: Column(children: [ @@ -66,10 +69,11 @@ class AccountPageState extends State { initiallyExpanded: true, children: [ ContactInvitationListWidget( - contactInvitationRecordList: contactInvitationRecordList) + contactInvitationRecordList: contactInvitationRecordList, + disabled: cilBusy) ], ).paddingLTRB(8, 0, 8, 8), - ContactListWidget(contactList: contactList).expanded(), + ContactListWidget(contactList: contactList, disabled: ciBusy).expanded(), ])); } } diff --git a/lib/tools/widget_helpers.dart b/lib/tools/widget_helpers.dart index 60ef937..b3bddb7 100644 --- a/lib/tools/widget_helpers.dart +++ b/lib/tools/widget_helpers.dart @@ -1,5 +1,6 @@ import 'package:async_tools/async_tools.dart'; import 'package:awesome_extensions/awesome_extensions.dart'; +import 'package:bloc_tools/bloc_tools.dart'; import 'package:blurry_modal_progress_hud/blurry_modal_progress_hud.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -77,6 +78,17 @@ extension AsyncValueBuilderExt on AsyncValue { data: (d) => debugPage('AsyncValue should not be data here')); } +extension BusyAsyncValueBuilderExt on BlocBusyState> { + Widget builder(Widget Function(BuildContext, T) builder) => + AbsorbPointer(absorbing: busy, child: state.builder(builder)); + Widget buildNotData( + {Widget Function()? loading, + Widget Function(Object, StackTrace?)? error}) => + AbsorbPointer( + absorbing: busy, + child: state.buildNotData(loading: loading, error: error)); +} + class AsyncBlocBuilder>, S> extends BlocBuilder> { AsyncBlocBuilder({ diff --git a/packages/async_tools/lib/async_tools.dart b/packages/async_tools/lib/async_tools.dart index 61d7e7b..70f7b61 100644 --- a/packages/async_tools/lib/async_tools.dart +++ b/packages/async_tools/lib/async_tools.dart @@ -6,3 +6,4 @@ export 'src/async_value.dart'; export 'src/serial_future.dart'; export 'src/single_future.dart'; export 'src/single_state_processor.dart'; +export 'src/single_stateless_processor.dart'; diff --git a/packages/async_tools/lib/src/single_state_processor.dart b/packages/async_tools/lib/src/single_state_processor.dart index ea1af10..18798fa 100644 --- a/packages/async_tools/lib/src/single_state_processor.dart +++ b/packages/async_tools/lib/src/single_state_processor.dart @@ -14,8 +14,7 @@ import '../async_tools.dart'; class SingleStateProcessor { SingleStateProcessor(); - void updateState(State newInputState, - {required Future Function(State) closure}) { + void updateState(State newInputState, Future Function(State) closure) { // Use a singlefuture here to ensure we get dont lose any updates // If the input stream gives us an update while we are // still processing the last update, the most recent input state will diff --git a/packages/async_tools/lib/src/single_stateless_processor.dart b/packages/async_tools/lib/src/single_stateless_processor.dart new file mode 100644 index 0000000..1b96b74 --- /dev/null +++ b/packages/async_tools/lib/src/single_stateless_processor.dart @@ -0,0 +1,47 @@ +import 'dart:async'; + +import '../async_tools.dart'; + +// Process a single stateless update at a time ensuring each request +// gets processed asynchronously, and continuously while update is requested. +// +// This is useful for processing updates asynchronously without waiting +// from a synchronous execution context +class SingleStatelessProcessor { + SingleStatelessProcessor(); + + void update(Future Function() closure) { + singleFuture(this, () async { + do { + _more = false; + await closure(); + + // See if another update was requested + } while (_more); + }, onBusy: () { + // Keep this state until we process again + _more = true; + }); + } + + // Like update, but with a busy wrapper that clears once the updating is finished + void busyUpdate( + Future Function(Future Function(void Function(S))) busy, + Future Function(void Function(S)) closure) { + singleFuture( + this, + () async => busy((emit) async { + do { + _more = false; + await closure(emit); + + // See if another update was requested + } while (_more); + }), onBusy: () { + // Keep this state until we process again + _more = true; + }); + } + + bool _more = false; +} diff --git a/packages/bloc_tools/lib/src/async_transformer_cubit.dart b/packages/bloc_tools/lib/src/async_transformer_cubit.dart index 83691c6..d32f37e 100644 --- a/packages/bloc_tools/lib/src/async_transformer_cubit.dart +++ b/packages/bloc_tools/lib/src/async_transformer_cubit.dart @@ -10,7 +10,7 @@ class AsyncTransformerCubit extends Cubit> { _subscription = input.stream.listen(_asyncTransform); } void _asyncTransform(AsyncValue newInputState) { - _singleStateProcessor.updateState(newInputState, closure: (newState) async { + _singleStateProcessor.updateState(newInputState, (newState) async { // Emit the transformed state try { if (newState is AsyncLoading) { diff --git a/packages/bloc_tools/lib/src/bloc_busy_wrapper.dart b/packages/bloc_tools/lib/src/bloc_busy_wrapper.dart index a6bb2d7..2307d0e 100644 --- a/packages/bloc_tools/lib/src/bloc_busy_wrapper.dart +++ b/packages/bloc_tools/lib/src/bloc_busy_wrapper.dart @@ -17,7 +17,7 @@ class BlocBusyState extends Equatable { } mixin BlocBusyWrapper on BlocBase> { - Future busy(Future Function(void Function(S) emit) closure) async => + Future busyValue(Future Function(void Function(S) emit) closure) => _mutex.protect(() async { void busyemit(S state) { changedState = state; @@ -41,6 +41,27 @@ mixin BlocBusyWrapper on BlocBase> { return out; }); + Future busy(Future Function(void Function(S) emit) closure) => + _mutex.protect(() async { + void busyemit(S state) { + changedState = state; + } + + // Turn on busy state + emit(BlocBusyState._busy(state.state)); + + // Run the closure + await closure(busyemit); + + // If the closure did one or more 'busy emits' then + // take the most recent one and emit it for real + final finalState = changedState; + if (finalState != null && finalState != state.state) { + emit(BlocBusyState._busy(finalState)); + } else { + emit(BlocBusyState._busy(state.state)); + } + }); void changeState(S state) { if (_mutex.isLocked) { changedState = state; @@ -49,6 +70,8 @@ mixin BlocBusyWrapper on BlocBase> { } } + bool get isBusy => _mutex.isLocked; + final Mutex _mutex = Mutex(); S? changedState; } diff --git a/packages/bloc_tools/lib/src/state_follower.dart b/packages/bloc_tools/lib/src/state_follower.dart index 04b8138..7f69f5e 100644 --- a/packages/bloc_tools/lib/src/state_follower.dart +++ b/packages/bloc_tools/lib/src/state_follower.dart @@ -31,7 +31,7 @@ abstract mixin class StateFollower { void _updateFollow(S newInputState) { _singleStateProcessor.updateState(getStateMap(newInputState), - closure: (newStateMap) async { + (newStateMap) async { for (final k in _lastInputStateMap.keys) { if (!newStateMap.containsKey(k)) { // deleted diff --git a/packages/veilid_support/lib/dht_support/src/dht_short_array_cubit.dart b/packages/veilid_support/lib/dht_support/src/dht_short_array_cubit.dart index 79837b2..8309254 100644 --- a/packages/veilid_support/lib/dht_support/src/dht_short_array_cubit.dart +++ b/packages/veilid_support/lib/dht_support/src/dht_short_array_cubit.dart @@ -4,18 +4,19 @@ import 'package:async_tools/async_tools.dart'; import 'package:bloc/bloc.dart'; import 'package:bloc_tools/bloc_tools.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; +import 'package:mutex/mutex.dart'; import '../../veilid_support.dart'; -class DHTShortArrayCubit extends Cubit>>> - with BlocBusyWrapper>> { +typedef DHTShortArrayState = AsyncValue>; +typedef DHTShortArrayBusyState = BlocBusyState>; + +class DHTShortArrayCubit extends Cubit> + with BlocBusyWrapper> { DHTShortArrayCubit({ required Future Function() open, required T Function(List data) decodeElement, }) : _decodeElement = decodeElement, - _wantsUpdate = false, - _isUpdating = false, - _wantsCloseRecord = false, super(const BlocBusyState(AsyncValue.loading())) { Future.delayed(Duration.zero, () async { // Open DHT record @@ -33,9 +34,6 @@ class DHTShortArrayCubit extends Cubit>>> required T Function(List data) decodeElement, }) : _shortArray = shortArray, _decodeElement = decodeElement, - _wantsUpdate = false, - _isUpdating = false, - _wantsCloseRecord = false, super(const BlocBusyState(AsyncValue.loading())) { // Make initial state update _update(); @@ -59,37 +57,21 @@ class DHTShortArrayCubit extends Cubit>>> void _update() { // Run at most one background update process - -xxx convert to singleFuture with onBusy that sets wantsupdate - - _wantsUpdate = true; - if (_isUpdating) { - return; - } - _isUpdating = true; - Future.delayed(Duration.zero, () async { - // Keep updating until we don't want to update any more - // Because this is async, we could get an update while we're - // still processing the last one + // Because this is async, we could get an update while we're + // still processing the last one + _sspUpdate.busyUpdate>>(busy, (emit) async { try { - do { - _wantsUpdate = false; - try { - final initialState = await _getElements(); - emit(AsyncValue.data(initialState)); - } on Exception catch (e) { - emit(AsyncValue.error(e)); - } - } while (_wantsUpdate); - } finally { - // Note that this update future has finished - _isUpdating = false; + final initialState = await _getElementsInner(); + emit(AsyncValue.data(initialState)); + } on Exception catch (e) { + emit(AsyncValue.error(e)); } }); } // Get and decode the entire short array - Future> _getElements() async { + Future> _getElementsInner() async { + assert(isBusy, 'should only be called from a busy state'); var out = IList(); for (var i = 0; i < _shortArray.length; i++) { // Get the element bytes (throw if fails, array state is invalid) @@ -112,12 +94,13 @@ xxx convert to singleFuture with onBusy that sets wantsupdate await super.close(); } - DHTShortArray get shortArray => _shortArray; + Future operate(Future Function(DHTShortArray) closure) async => + _operateMutex.protect(() async => closure(_shortArray)); + final _operateMutex = Mutex(); late final DHTShortArray _shortArray; final T Function(List data) _decodeElement; StreamSubscription? _subscription; - bool _wantsUpdate; - bool _isUpdating; - bool _wantsCloseRecord; + bool _wantsCloseRecord = false; + final _sspUpdate = SingleStatelessProcessor(); } From efb7b9598017c6f52335070fc6a2ff8789809137 Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Tue, 27 Feb 2024 21:37:39 -0500 Subject: [PATCH 49/68] bugfixes flutter 3.19 upgrade --- lib/chat/cubits/messages_cubit.dart | 12 ++- .../views/contact_invitation_item_widget.dart | 53 +++++++------ lib/contacts/cubits/conversation_cubit.dart | 10 ++- lib/contacts/views/contact_item_widget.dart | 4 +- .../home_account_ready_shell.dart | 22 ++---- macos/Podfile.lock | 2 +- macos/Runner.xcodeproj/project.pbxproj | 2 +- .../xcshareddata/xcschemes/Runner.xcscheme | 2 +- .../bloc_tools/lib/src/bloc_busy_wrapper.dart | 7 +- .../bloc_tools/lib/src/state_follower.dart | 4 + packages/bloc_tools/pubspec.yaml | 2 +- packages/mutex/pubspec.yaml | 2 +- .../lib/dht_support/src/dht_record.dart | 2 +- packages/veilid_support/pubspec.lock | 50 +++++------- packages/veilid_support/pubspec.yaml | 2 +- pubspec.lock | 76 +++++++++---------- pubspec.yaml | 6 +- 17 files changed, 131 insertions(+), 127 deletions(-) diff --git a/lib/chat/cubits/messages_cubit.dart b/lib/chat/cubits/messages_cubit.dart index febc23e..c1528de 100644 --- a/lib/chat/cubits/messages_cubit.dart +++ b/lib/chat/cubits/messages_cubit.dart @@ -59,6 +59,8 @@ class MessagesCubit extends Cubit>> { @override Future close() async { + await _localSubscription?.cancel(); + await _remoteSubscription?.cancel(); await super.close(); } @@ -132,7 +134,8 @@ class MessagesCubit extends Cubit>> { _localMessagesCubit = DHTShortArrayCubit.value( shortArray: localMessagesRecord, decodeElement: proto.Message.fromBuffer); - _localMessagesCubit!.stream.listen(updateLocalMessagesState); + _localSubscription = + _localMessagesCubit!.stream.listen(updateLocalMessagesState); } // Open remote messages key @@ -141,7 +144,8 @@ class MessagesCubit extends Cubit>> { _remoteMessagesCubit = DHTShortArrayCubit.value( shortArray: remoteMessagesRecord, decodeElement: proto.Message.fromBuffer); - _remoteMessagesCubit!.stream.listen(updateRemoteMessagesState); + _remoteSubscription = + _remoteMessagesCubit!.stream.listen(updateRemoteMessagesState); } // Initialize local messages @@ -209,6 +213,10 @@ class MessagesCubit extends Cubit>> { DHTShortArrayCubit? _localMessagesCubit; DHTShortArrayCubit? _remoteMessagesCubit; final StreamController<_MessageQueueEntry> _remoteMessagesQueue; + StreamSubscription>>>? + _localSubscription; + StreamSubscription>>>? + _remoteSubscription; // DHTRecordCrypto? _messagesCrypto; } diff --git a/lib/contact_invitation/views/contact_invitation_item_widget.dart b/lib/contact_invitation/views/contact_invitation_item_widget.dart index c6e96c1..2ed25c5 100644 --- a/lib/contact_invitation/views/contact_invitation_item_widget.dart +++ b/lib/contact_invitation/views/contact_invitation_item_widget.dart @@ -53,15 +53,18 @@ class ContactInvitationItemWidget extends StatelessWidget { children: [ // A SlidableAction can have an icon and/or a label. SlidableAction( - onPressed: (context) async { - final contactInvitationListCubit = - context.read(); - await contactInvitationListCubit.deleteInvitation( - accepted: false, - contactRequestInboxRecordKey: contactInvitationRecord - .contactRequestInbox.recordKey - .toVeilid()); - }, + onPressed: disabled + ? null + : (context) async { + final contactInvitationListCubit = + context.read(); + await contactInvitationListCubit.deleteInvitation( + accepted: false, + contactRequestInboxRecordKey: + contactInvitationRecord + .contactRequestInbox.recordKey + .toVeilid()); + }, backgroundColor: scale.tertiaryScale.background, foregroundColor: scale.tertiaryScale.text, icon: Icons.delete, @@ -96,21 +99,23 @@ class ContactInvitationItemWidget extends StatelessWidget { // component is not dragged. child: ListTile( //title: Text(translate('contact_list.invitation')), - onTap: () async { - // ignore: use_build_context_synchronously - if (!context.mounted) { - return; - } - await showDialog( - context: context, - builder: (context) => BlocProvider( - create: (context) => InvitationGeneratorCubit( - Future.value(Uint8List.fromList( - contactInvitationRecord.invitation))), - child: ContactInvitationDisplayDialog( - message: contactInvitationRecord.message, - ))); - }, + onTap: disabled + ? null + : () async { + // ignore: use_build_context_synchronously + if (!context.mounted) { + return; + } + await showDialog( + context: context, + builder: (context) => BlocProvider( + create: (context) => InvitationGeneratorCubit( + Future.value(Uint8List.fromList( + contactInvitationRecord.invitation))), + child: ContactInvitationDisplayDialog( + message: contactInvitationRecord.message, + ))); + }, title: Text( contactInvitationRecord.message.isEmpty ? translate('contact_list.invitation') diff --git a/lib/contacts/cubits/conversation_cubit.dart b/lib/contacts/cubits/conversation_cubit.dart index 91ae098..8994209 100644 --- a/lib/contacts/cubits/conversation_cubit.dart +++ b/lib/contacts/cubits/conversation_cubit.dart @@ -73,6 +73,8 @@ class ConversationCubit extends Cubit> { @override Future close() async { + await _localSubscription?.cancel(); + await _remoteSubscription?.cancel(); await super.close(); } @@ -127,7 +129,8 @@ class ConversationCubit extends Cubit> { _localConversationCubit = DefaultDHTRecordCubit.value( record: localConversationRecord, decodeState: proto.Conversation.fromBuffer); - _localConversationCubit!.stream.listen(updateLocalConversationState); + _localSubscription = + _localConversationCubit!.stream.listen(updateLocalConversationState); } // Open remote converation key @@ -138,7 +141,8 @@ class ConversationCubit extends Cubit> { _remoteConversationCubit = DefaultDHTRecordCubit.value( record: remoteConversationRecord, decodeState: proto.Conversation.fromBuffer); - _remoteConversationCubit!.stream.listen(updateRemoteConversationState); + _remoteSubscription = + _remoteConversationCubit!.stream.listen(updateRemoteConversationState); } // Initialize a local conversation @@ -265,6 +269,8 @@ class ConversationCubit extends Cubit> { final TypedKey? _remoteConversationRecordKey; DefaultDHTRecordCubit? _localConversationCubit; DefaultDHTRecordCubit? _remoteConversationCubit; + StreamSubscription>? _localSubscription; + StreamSubscription>? _remoteSubscription; ConversationState _incrementalState; // DHTRecordCrypto? _conversationCrypto; diff --git a/lib/contacts/views/contact_item_widget.dart b/lib/contacts/views/contact_item_widget.dart index 4fc2bce..49d6bb1 100644 --- a/lib/contacts/views/contact_item_widget.dart +++ b/lib/contacts/views/contact_item_widget.dart @@ -43,7 +43,7 @@ class ContactItemWidget extends StatelessWidget { motion: const DrawerMotion(), children: [ SlidableAction( - onPressed: disabled || context.read().isBusy + onPressed: disabled || context.watch().isBusy ? null : (context) async { final contactListCubit = @@ -77,7 +77,7 @@ class ContactItemWidget extends StatelessWidget { // The child of the Slidable is what the user sees when the // component is not dragged. child: ListTile( - onTap: disabled || context.read().isBusy + onTap: disabled || context.watch().isBusy ? null : () async { // Start a chat diff --git a/lib/layout/home/home_account_ready/home_account_ready_shell.dart b/lib/layout/home/home_account_ready/home_account_ready_shell.dart index 4fee8ab..7f88cd6 100644 --- a/lib/layout/home/home_account_ready/home_account_ready_shell.dart +++ b/lib/layout/home/home_account_ready/home_account_ready_shell.dart @@ -1,6 +1,4 @@ import 'package:async_tools/async_tools.dart'; -import 'package:bloc_tools/bloc_tools.dart'; -import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -145,19 +143,13 @@ class HomeAccountReadyShellState extends State { create: (context) => ActiveConversationsBlocMapCubit( activeAccountInfo: widget.activeAccountInfo, contactListCubit: context.read()) - ..follow( - initialInputState: - const BlocBusyState(AsyncValue.loading()), - stream: context.read().stream)), + ..followBloc(context.read())), BlocProvider( create: (context) => ActiveConversationMessagesBlocMapCubit( activeAccountInfo: widget.activeAccountInfo, - )..follow( - initialInputState: IMap(), - stream: context - .read() - .stream)), + )..followBloc( + context.read())), BlocProvider( create: (context) => ActiveChatCubit(null) ..withStateListen((event) { @@ -167,12 +159,8 @@ class HomeAccountReadyShellState extends State { create: (context) => WaitingInvitationsBlocMapCubit( activeAccountInfo: widget.activeAccountInfo, account: account) - ..follow( - initialInputState: - const BlocBusyState(AsyncValue.loading()), - stream: context - .read() - .stream)) + ..followBloc( + context.read())) ], child: MultiBlocListener(listeners: [ BlocListener on BlocBase> { await closure(busyemit); // If the closure did one or more 'busy emits' then - // take the most recent one and emit it for real + // take the most recent one and emit it for real and + // turn off the busy state final finalState = changedState; if (finalState != null && finalState != state.state) { - emit(BlocBusyState._busy(finalState)); + emit(BlocBusyState(finalState)); } else { - emit(BlocBusyState._busy(state.state)); + emit(BlocBusyState(state.state)); } }); void changeState(S state) { diff --git a/packages/bloc_tools/lib/src/state_follower.dart b/packages/bloc_tools/lib/src/state_follower.dart index 7f69f5e..9ae3fee 100644 --- a/packages/bloc_tools/lib/src/state_follower.dart +++ b/packages/bloc_tools/lib/src/state_follower.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'package:async_tools/async_tools.dart'; +import 'package:bloc/bloc.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; // Mixin that automatically keeps two blocs/cubits in sync with each other @@ -21,6 +22,9 @@ abstract mixin class StateFollower { _subscription = stream.listen(_updateFollow); } + void followBloc>(B bloc) => + follow(initialInputState: bloc.state, stream: bloc.stream); + Future close() async { await _subscription.cancel(); } diff --git a/packages/bloc_tools/pubspec.yaml b/packages/bloc_tools/pubspec.yaml index 73bbe16..a51fa35 100644 --- a/packages/bloc_tools/pubspec.yaml +++ b/packages/bloc_tools/pubspec.yaml @@ -13,7 +13,7 @@ dependencies: equatable: ^2.0.5 fast_immutable_collections: ^10.1.1 freezed_annotation: ^2.4.1 - meta: ^1.10.0 + meta: ^1.11.0 mutex: path: ../mutex diff --git a/packages/mutex/pubspec.yaml b/packages/mutex/pubspec.yaml index 30db4a2..753ae25 100644 --- a/packages/mutex/pubspec.yaml +++ b/packages/mutex/pubspec.yaml @@ -8,5 +8,5 @@ environment: dev_dependencies: lint_hard: ^4.0.0 - pana: ^0.21.45 + pana: ^0.22.2 test: ^1.25.2 diff --git a/packages/veilid_support/lib/dht_support/src/dht_record.dart b/packages/veilid_support/lib/dht_support/src/dht_record.dart index 325d6e8..20d0524 100644 --- a/packages/veilid_support/lib/dht_support/src/dht_record.dart +++ b/packages/veilid_support/lib/dht_support/src/dht_record.dart @@ -385,6 +385,6 @@ class DHTRecord { void _addRemoteValueChange(VeilidUpdateValueChange update) { _addValueChange( - local: false, data: update.valueData.data, subkeys: update.subkeys); + local: false, data: update.value.data, subkeys: update.subkeys); } } diff --git a/packages/veilid_support/pubspec.lock b/packages/veilid_support/pubspec.lock index ede8275..a6ec0d8 100644 --- a/packages/veilid_support/pubspec.lock +++ b/packages/veilid_support/pubspec.lock @@ -5,18 +5,18 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - sha256: eb376e9acf6938204f90eb3b1f00b578640d3188b4c8a8ec054f9f479af8d051 + sha256: "0b2f2bd91ba804e53a61d757b986f89f1f9eaed5b11e4b2f5a2468d86d6c9fc7" url: "https://pub.dev" source: hosted - version: "64.0.0" + version: "67.0.0" analyzer: dependency: transitive description: name: analyzer - sha256: "69f54f967773f6c26c7dcb13e93d7ccee8b17a641689da39e878d5cf13b06893" + sha256: "37577842a27e4338429a1cbc32679d508836510b056f1eedf0c8d20e39c1383d" url: "https://pub.dev" source: hosted - version: "6.2.0" + version: "6.4.1" args: dependency: transitive description: @@ -131,10 +131,10 @@ packages: dependency: transitive description: name: change_case - sha256: f4e08feaa845e75e4f5ad2b0e15f24813d7ea6c27e7b78252f0c17f752cf1157 + sha256: "47c48c36f95f20c6d0ba03efabceff261d05026cca322cc2c4c01c343371b5bb" url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "2.0.1" characters: dependency: transitive description: @@ -227,10 +227,10 @@ packages: dependency: transitive description: name: ffi - sha256: "7bf0adc28a23d395f19f3f1eb21dd7cfd1dd9f8e1c50051c069122e6853bc878" + sha256: "493f37e7df1804778ff3a53bd691d8692ddf69702cf4c1c1096a2e41b4779e21" url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.1.2" file: dependency: transitive description: @@ -239,14 +239,6 @@ packages: url: "https://pub.dev" source: hosted version: "7.0.0" - file_utils: - dependency: transitive - description: - name: file_utils - sha256: d1e64389a22649095c8405c9e177272caf05139255931c9ff30d53b5c9bcaa34 - url: "https://pub.dev" - source: hosted - version: "1.0.1" fixnum: dependency: transitive description: @@ -397,18 +389,18 @@ packages: dependency: transitive description: name: material_color_utilities - sha256: "9528f2f296073ff54cb9fee677df673ace1218163c3bc7628093e7eed5203d41" + sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a" url: "https://pub.dev" source: hosted - version: "0.5.0" + version: "0.8.0" meta: dependency: "direct main" description: name: meta - sha256: a6e590c838b18133bb482a2745ad77c5bb7715fb0451209e1a7567d416678b8e + sha256: d584fa6707a52763a52446f02cc621b077888fb63b93bbcb1143a7be5a0c0c04 url: "https://pub.dev" source: hosted - version: "1.10.0" + version: "1.11.0" mime: dependency: transitive description: @@ -657,10 +649,10 @@ packages: dependency: transitive description: name: system_info2 - sha256: af2f948e3f31a3367a049932a8ad59faf0063ecf836a020d975b9f41566d8bc9 + sha256: "65206bbef475217008b5827374767550a5420ce70a04d2d7e94d1d2253f3efc9" url: "https://pub.dev" source: hosted - version: "3.0.2" + version: "4.0.0" system_info_plus: dependency: transitive description: @@ -736,10 +728,10 @@ packages: dependency: transitive description: name: vm_service - sha256: a2662fb1f114f4296cf3f5a50786a2d888268d7776cf681aa17d660ffa23b246 + sha256: e7d5ecd604e499358c5fe35ee828c0298a320d54455e791e9dcf73486bc8d9f0 url: "https://pub.dev" source: hosted - version: "14.0.0" + version: "14.1.0" watcher: dependency: transitive description: @@ -752,18 +744,18 @@ packages: dependency: transitive description: name: web - sha256: afe077240a270dcfd2aafe77602b4113645af95d0ad31128cc02bce5ac5d5152 + sha256: "1d9158c616048c38f712a6646e317a3426da10e884447626167240d45209cbad" url: "https://pub.dev" source: hosted - version: "0.3.0" + version: "0.5.0" web_socket_channel: dependency: transitive description: name: web_socket_channel - sha256: d88238e5eac9a42bb43ca4e721edba3c08c6354d4a53063afaa568516217621b + sha256: "1d8e795e2a8b3730c41b8a98a2dff2e0fb57ae6f0764a1c46ec5915387d257b2" url: "https://pub.dev" source: hosted - version: "2.4.0" + version: "2.4.4" webkit_inspection_protocol: dependency: transitive description: @@ -797,5 +789,5 @@ packages: source: hosted version: "3.1.2" sdks: - dart: ">=3.2.0 <4.0.0" + dart: ">=3.3.0 <4.0.0" flutter: ">=3.10.6" diff --git a/packages/veilid_support/pubspec.yaml b/packages/veilid_support/pubspec.yaml index 7d03995..d7344ef 100644 --- a/packages/veilid_support/pubspec.yaml +++ b/packages/veilid_support/pubspec.yaml @@ -17,7 +17,7 @@ dependencies: freezed_annotation: ^2.4.1 json_annotation: ^4.8.1 loggy: ^2.0.3 - meta: ^1.10.0 + meta: ^1.11.0 mutex: path: ../mutex diff --git a/pubspec.lock b/pubspec.lock index 4b6dbd1..e6de13e 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -5,18 +5,18 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - sha256: eb376e9acf6938204f90eb3b1f00b578640d3188b4c8a8ec054f9f479af8d051 + sha256: "0b2f2bd91ba804e53a61d757b986f89f1f9eaed5b11e4b2f5a2468d86d6c9fc7" url: "https://pub.dev" source: hosted - version: "64.0.0" + version: "67.0.0" analyzer: dependency: transitive description: name: analyzer - sha256: "69f54f967773f6c26c7dcb13e93d7ccee8b17a641689da39e878d5cf13b06893" + sha256: "37577842a27e4338429a1cbc32679d508836510b056f1eedf0c8d20e39c1383d" url: "https://pub.dev" source: hosted - version: "6.2.0" + version: "6.4.1" animated_theme_switcher: dependency: "direct main" description: @@ -347,10 +347,10 @@ packages: dependency: transitive description: name: cross_file - sha256: fedaadfa3a6996f75211d835aaeb8fede285dae94262485698afd832371b9a5e + sha256: "55d7b444feb71301ef6b8838dbc1ae02e63dd48c8773f3810ff53bb1e2945b32" url: "https://pub.dev" source: hosted - version: "0.3.3+8" + version: "0.3.4+1" crypto: dependency: transitive description: @@ -411,10 +411,10 @@ packages: dependency: transitive description: name: ffi - sha256: "7bf0adc28a23d395f19f3f1eb21dd7cfd1dd9f8e1c50051c069122e6853bc878" + sha256: "493f37e7df1804778ff3a53bd691d8692ddf69702cf4c1c1096a2e41b4779e21" url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.1.2" file: dependency: transitive description: @@ -674,10 +674,10 @@ packages: dependency: transitive description: name: http - sha256: a2bbf9d017fcced29139daa8ed2bba4ece450ab222871df93ca9eec6f80c34ba + sha256: "761a297c042deedc1ffbb156d6e2af13886bb305c2a343a4d972504cd67dd938" url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.2.1" http_multi_server: dependency: transitive description: @@ -802,18 +802,18 @@ packages: dependency: transitive description: name: material_color_utilities - sha256: "9528f2f296073ff54cb9fee677df673ace1218163c3bc7628093e7eed5203d41" + sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a" url: "https://pub.dev" source: hosted - version: "0.5.0" + version: "0.8.0" meta: dependency: "direct main" description: name: meta - sha256: a6e590c838b18133bb482a2745ad77c5bb7715fb0451209e1a7567d416678b8e + sha256: d584fa6707a52763a52446f02cc621b077888fb63b93bbcb1143a7be5a0c0c04 url: "https://pub.dev" source: hosted - version: "1.10.0" + version: "1.11.0" mime: dependency: transitive description: @@ -881,10 +881,10 @@ packages: dependency: "direct main" description: name: path - sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917" + sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" url: "https://pub.dev" source: hosted - version: "1.8.3" + version: "1.9.0" path_parsing: dependency: transitive description: @@ -973,14 +973,6 @@ packages: url: "https://pub.dev" source: hosted version: "3.1.4" - platform_info: - dependency: transitive - description: - name: platform_info - sha256: "012e73712166cf0b56d3eb95c0d33491f56b428c169eca385f036448474147e4" - url: "https://pub.dev" - source: hosted - version: "3.2.0" plugin_platform_interface: dependency: transitive description: @@ -1193,10 +1185,10 @@ packages: dependency: transitive description: name: shared_preferences_web - sha256: "7b15ffb9387ea3e237bb7a66b8a23d2147663d391cafc5c8f37b2e7b4bde5d21" + sha256: "9aee1089b36bd2aafe06582b7d7817fd317ef05fc30e6ba14bff247d0933042a" url: "https://pub.dev" source: hosted - version: "2.2.2" + version: "2.3.0" shared_preferences_windows: dependency: transitive description: @@ -1430,10 +1422,10 @@ packages: dependency: transitive description: name: url_launcher_ios - sha256: "75bb6fe3f60070407704282a2d295630cab232991eb52542b18347a8a941df03" + sha256: "9149d493b075ed740901f3ee844a38a00b33116c7c5c10d7fb27df8987fb51d5" url: "https://pub.dev" source: hosted - version: "6.2.4" + version: "6.2.5" url_launcher_linux: dependency: transitive description: @@ -1462,10 +1454,10 @@ packages: dependency: transitive description: name: url_launcher_web - sha256: fff0932192afeedf63cdd50ecbb1bc825d31aed259f02bb8dba0f3b729a5e88b + sha256: "3692a459204a33e04bc94f5fb91158faf4f2c8903281ddd82915adecdb1a901d" url: "https://pub.dev" source: hosted - version: "2.2.3" + version: "2.3.0" url_launcher_windows: dependency: transitive description: @@ -1548,18 +1540,18 @@ packages: dependency: transitive description: name: web - sha256: afe077240a270dcfd2aafe77602b4113645af95d0ad31128cc02bce5ac5d5152 + sha256: "1d9158c616048c38f712a6646e317a3426da10e884447626167240d45209cbad" url: "https://pub.dev" source: hosted - version: "0.3.0" + version: "0.5.0" web_socket_channel: dependency: transitive description: name: web_socket_channel - sha256: d88238e5eac9a42bb43ca4e721edba3c08c6354d4a53063afaa568516217621b + sha256: "1d8e795e2a8b3730c41b8a98a2dff2e0fb57ae6f0764a1c46ec5915387d257b2" url: "https://pub.dev" source: hosted - version: "2.4.0" + version: "2.4.4" win32: dependency: transitive description: @@ -1596,10 +1588,10 @@ packages: dependency: "direct main" description: name: xterm - sha256: "6a02b15d03152b8186e12790902ff28c8a932fc441e89fa7255a7491661a8e69" + sha256: "168dfedca77cba33fdb6f52e2cd001e9fde216e398e89335c19b524bb22da3a2" url: "https://pub.dev" source: hosted - version: "3.5.0" + version: "4.0.0" yaml: dependency: transitive description: @@ -1608,6 +1600,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.1.2" + zmodem: + dependency: transitive + description: + name: zmodem + sha256: "3b7e5b29f3a7d8aee472029b05165a68438eff2f3f7766edf13daba1e297adbf" + url: "https://pub.dev" + source: hosted + version: "0.0.6" zxing2: dependency: "direct main" description: @@ -1625,5 +1625,5 @@ packages: source: hosted version: "1.1.2" sdks: - dart: ">=3.2.3 <4.0.0" - flutter: ">=3.16.6" + dart: ">=3.3.0 <4.0.0" + flutter: ">=3.19.0" diff --git a/pubspec.yaml b/pubspec.yaml index 68cbd1c..e25b649 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -52,13 +52,13 @@ dependencies: intl: ^0.18.1 json_annotation: ^4.8.1 loggy: ^2.0.3 - meta: ^1.10.0 + meta: ^1.11.0 mobile_scanner: ^4.0.0 motion_toast: ^2.8.0 mutex: path: packages/mutex pasteboard: ^0.2.0 - path: ^1.8.3 + path: ^1.9.0 path_provider: ^2.1.2 pinput: ^4.0.0 preload_page_view: ^0.2.0 @@ -84,7 +84,7 @@ dependencies: veilid_support: path: packages/veilid_support window_manager: ^0.3.8 - xterm: ^3.5.0 + xterm: ^4.0.0 zxing2: ^0.2.3 dev_dependencies: From 72ffae6a928b6c9c01450a3392ea0054f4b11e15 Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Tue, 27 Feb 2024 22:12:04 -0500 Subject: [PATCH 50/68] provider debugger --- devtools_options.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/devtools_options.yaml b/devtools_options.yaml index 7e7e7f6..5c27c3e 100644 --- a/devtools_options.yaml +++ b/devtools_options.yaml @@ -1 +1,2 @@ extensions: + - provider: true \ No newline at end of file From ad9a77d68f012ac479579a8f5f26d62a743abd9c Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Wed, 28 Feb 2024 11:58:46 -0500 Subject: [PATCH 51/68] fix follow --- lib/chat_list/cubits/active_conversations_bloc_map_cubit.dart | 4 ++-- packages/bloc_tools/lib/src/state_follower.dart | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/chat_list/cubits/active_conversations_bloc_map_cubit.dart b/lib/chat_list/cubits/active_conversations_bloc_map_cubit.dart index 7f67f10..8212331 100644 --- a/lib/chat_list/cubits/active_conversations_bloc_map_cubit.dart +++ b/lib/chat_list/cubits/active_conversations_bloc_map_cubit.dart @@ -46,7 +46,7 @@ class ActiveConversationsBlocMapCubit extends BlocMapCubit addConversation({required proto.Contact contact}) async => + Future _addConversation({required proto.Contact contact}) async => add(() => MapEntry( contact.remoteConversationRecordKey.toVeilid(), TransformerCubit( @@ -103,7 +103,7 @@ class ActiveConversationsBlocMapCubit extends BlocMapCubit { required Stream stream, }) { // - _lastInputStateMap = getStateMap(initialInputState); + _lastInputStateMap = IMap(); + _updateFollow(initialInputState); _subscription = stream.listen(_updateFollow); } From d00722433da046cc727261c9e0c52aedf9415748 Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Wed, 28 Feb 2024 20:32:37 -0500 Subject: [PATCH 52/68] clean up windows and loading state --- .../new_account_page/new_account_page.dart | 1 - lib/chat/cubits/active_chat_cubit.dart | 1 - lib/chat/views/chat_component.dart | 2 +- lib/chat_list/cubits/chat_list_cubit.dart | 44 ++++-- .../home_account_ready_chat.dart | 6 + .../home_account_ready_main.dart | 17 +- .../home_account_ready_shell.dart | 11 +- lib/layout/index.dart | 19 ++- lib/settings/settings_page.dart | 5 + lib/tick.dart | 146 +----------------- lib/veilid_processor/views/developer.dart | 10 +- .../bloc_tools/lib/src/state_follower.dart | 2 +- .../lib/dht_support/src/dht_record_pool.dart | 41 ++++- 13 files changed, 130 insertions(+), 175 deletions(-) diff --git a/lib/account_manager/views/new_account_page/new_account_page.dart b/lib/account_manager/views/new_account_page/new_account_page.dart index ab1a841..b57c5fa 100644 --- a/lib/account_manager/views/new_account_page/new_account_page.dart +++ b/lib/account_manager/views/new_account_page/new_account_page.dart @@ -29,7 +29,6 @@ class NewAccountPageState extends State { super.initState(); WidgetsBinding.instance.addPostFrameCallback((_) async { - setState(() {}); await changeWindowSetup( TitleBarStyle.normal, OrientationCapability.portraitOnly); }); diff --git a/lib/chat/cubits/active_chat_cubit.dart b/lib/chat/cubits/active_chat_cubit.dart index 5c7119d..b57076e 100644 --- a/lib/chat/cubits/active_chat_cubit.dart +++ b/lib/chat/cubits/active_chat_cubit.dart @@ -2,7 +2,6 @@ import 'package:bloc_tools/bloc_tools.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:veilid_support/veilid_support.dart'; - class ActiveChatCubit extends Cubit with BlocTools { ActiveChatCubit(super.initialState); diff --git a/lib/chat/views/chat_component.dart b/lib/chat/views/chat_component.dart index ab3d16f..03a05ba 100644 --- a/lib/chat/views/chat_component.dart +++ b/lib/chat/views/chat_component.dart @@ -57,7 +57,7 @@ class ChatComponent extends StatelessWidget { AsyncValue?>( (x) => x.state[remoteConversationRecordKey]); if (avconversation == null) { - return debugPage('should always have an active conversation here'); + return waitingPage(); } final conversation = avconversation.data?.value; if (conversation == null) { diff --git a/lib/chat_list/cubits/chat_list_cubit.dart b/lib/chat_list/cubits/chat_list_cubit.dart index 11eebad..286f03b 100644 --- a/lib/chat_list/cubits/chat_list_cubit.dart +++ b/lib/chat_list/cubits/chat_list_cubit.dart @@ -3,6 +3,7 @@ import 'dart:async'; import 'package:veilid_support/veilid_support.dart'; import '../../account_manager/account_manager.dart'; +import '../../chat/chat.dart'; import '../../proto/proto.dart' as proto; ////////////////////////////////////////////////// @@ -14,6 +15,7 @@ class ChatListCubit extends DHTShortArrayCubit { ChatListCubit({ required ActiveAccountInfo activeAccountInfo, required proto.Account account, + required this.activeChatCubit, }) : super( open: () => _open(activeAccountInfo, account), decodeElement: proto.Chat.fromBuffer); @@ -35,18 +37,35 @@ class ChatListCubit extends DHTShortArrayCubit { Future getOrCreateChatSingleContact({ required TypedKey remoteConversationRecordKey, }) async { - // Create conversation type Chat - final chat = proto.Chat() - ..type = proto.ChatType.SINGLE_CONTACT - ..remoteConversationKey = remoteConversationRecordKey.toProto(); - // Add Chat to account's list // if this fails, don't keep retrying, user can try again later - final added = await operate( - (shortArray) => shortArray.tryAddItem(chat.writeToBuffer())); - if (!added) { - throw Exception('Failed to add chat'); - } + await operate((shortArray) async { + final remoteConversationRecordKeyProto = + remoteConversationRecordKey.toProto(); + + // See if we have added this chat already + for (var i = 0; i < shortArray.length; i++) { + final cbuf = await shortArray.getItem(i); + if (cbuf == null) { + throw Exception('Failed to get chat'); + } + final c = proto.Chat.fromBuffer(cbuf); + if (c.remoteConversationKey == remoteConversationRecordKeyProto) { + // Nothing to do here + return; + } + } + // Create conversation type Chat + final chat = proto.Chat() + ..type = proto.ChatType.SINGLE_CONTACT + ..remoteConversationKey = remoteConversationRecordKeyProto; + + // Add chat + final added = await shortArray.tryAddItem(chat.writeToBuffer()); + if (!added) { + throw Exception('Failed to add chat'); + } + }); } /// Delete a chat @@ -58,6 +77,9 @@ class ChatListCubit extends DHTShortArrayCubit { // Remove Chat from account's list // if this fails, don't keep retrying, user can try again later await operate((shortArray) async { + if (activeChatCubit.state == remoteConversationRecordKey) { + activeChatCubit.setActiveChat(null); + } for (var i = 0; i < shortArray.length; i++) { final cbuf = await shortArray.getItem(i); if (cbuf == null) { @@ -71,4 +93,6 @@ class ChatListCubit extends DHTShortArrayCubit { } }); } + + final ActiveChatCubit activeChatCubit; } diff --git a/lib/layout/home/home_account_ready/home_account_ready_chat.dart b/lib/layout/home/home_account_ready/home_account_ready_chat.dart index d046b02..ffeaa05 100644 --- a/lib/layout/home/home_account_ready/home_account_ready_chat.dart +++ b/lib/layout/home/home_account_ready/home_account_ready_chat.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import '../../../chat/chat.dart'; +import '../../../tools/tools.dart'; class HomeAccountReadyChat extends StatefulWidget { const HomeAccountReadyChat({super.key}); @@ -16,6 +17,11 @@ class HomeAccountReadyChatState extends State { @override void initState() { super.initState(); + + WidgetsBinding.instance.addPostFrameCallback((_) async { + await changeWindowSetup( + TitleBarStyle.normal, OrientationCapability.normal); + }); } @override diff --git a/lib/layout/home/home_account_ready/home_account_ready_main.dart b/lib/layout/home/home_account_ready/home_account_ready_main.dart index 2a5ee52..d02cfaf 100644 --- a/lib/layout/home/home_account_ready/home_account_ready_main.dart +++ b/lib/layout/home/home_account_ready/home_account_ready_main.dart @@ -10,9 +10,24 @@ import '../../../theme/theme.dart'; import '../../../tools/tools.dart'; import 'main_pager/main_pager.dart'; -class HomeAccountReadyMain extends StatelessWidget { +class HomeAccountReadyMain extends StatefulWidget { const HomeAccountReadyMain({super.key}); + @override + State createState() => _HomeAccountReadyMainState(); +} + +class _HomeAccountReadyMainState extends State { + @override + void initState() { + super.initState(); + + WidgetsBinding.instance.addPostFrameCallback((_) async { + await changeWindowSetup( + TitleBarStyle.normal, OrientationCapability.normal); + }); + } + Widget buildUserPanel() => Builder(builder: (context) { final account = context.watch().state; final theme = Theme.of(context); diff --git a/lib/layout/home/home_account_ready/home_account_ready_shell.dart b/lib/layout/home/home_account_ready/home_account_ready_shell.dart index 7f88cd6..1f27fce 100644 --- a/lib/layout/home/home_account_ready/home_account_ready_shell.dart +++ b/lib/layout/home/home_account_ready/home_account_ready_shell.dart @@ -135,9 +135,15 @@ class HomeAccountReadyShellState extends State { create: (context) => ContactListCubit( activeAccountInfo: widget.activeAccountInfo, account: account)), + BlocProvider( + create: (context) => ActiveChatCubit(null) + ..withStateListen((event) { + widget.routerCubit.setHasActiveChat(event != null); + })), BlocProvider( create: (context) => ChatListCubit( activeAccountInfo: widget.activeAccountInfo, + activeChatCubit: context.read(), account: account)), BlocProvider( create: (context) => ActiveConversationsBlocMapCubit( @@ -150,11 +156,6 @@ class HomeAccountReadyShellState extends State { activeAccountInfo: widget.activeAccountInfo, )..followBloc( context.read())), - BlocProvider( - create: (context) => ActiveChatCubit(null) - ..withStateListen((event) { - widget.routerCubit.setHasActiveChat(event != null); - })), BlocProvider( create: (context) => WaitingInvitationsBlocMapCubit( activeAccountInfo: widget.activeAccountInfo, diff --git a/lib/layout/index.dart b/lib/layout/index.dart index 3690d70..958b909 100644 --- a/lib/layout/index.dart +++ b/lib/layout/index.dart @@ -2,9 +2,26 @@ import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:radix_colors/radix_colors.dart'; -class IndexPage extends StatelessWidget { +import '../tools/tools.dart'; + +class IndexPage extends StatefulWidget { const IndexPage({super.key}); + @override + State createState() => _IndexPageState(); +} + +class _IndexPageState extends State { + @override + void initState() { + super.initState(); + + WidgetsBinding.instance.addPostFrameCallback((_) async { + await changeWindowSetup( + TitleBarStyle.hidden, OrientationCapability.normal); + }); + } + @override Widget build(BuildContext context) { final theme = Theme.of(context); diff --git a/lib/settings/settings_page.dart b/lib/settings/settings_page.dart index b17b4a1..821f9c6 100644 --- a/lib/settings/settings_page.dart +++ b/lib/settings/settings_page.dart @@ -25,6 +25,11 @@ class SettingsPageState extends State { @override void initState() { super.initState(); + + WidgetsBinding.instance.addPostFrameCallback((_) async { + await changeWindowSetup( + TitleBarStyle.normal, OrientationCapability.normal); + }); } @override diff --git a/lib/tick.dart b/lib/tick.dart index bb495a2..99007e7 100644 --- a/lib/tick.dart +++ b/lib/tick.dart @@ -7,9 +7,6 @@ import 'package:veilid_support/veilid_support.dart'; import 'init.dart'; import 'veilid_processor/veilid_processor.dart'; -const int ticksPerContactInvitationCheck = 5; -const int ticksPerNewMessageCheck = 5; - class BackgroundTicker extends StatefulWidget { const BackgroundTicker({required this.builder, super.key}); @@ -28,11 +25,6 @@ class BackgroundTicker extends StatefulWidget { class BackgroundTickerState extends State { Timer? _tickTimer; bool _inTick = false; - bool _inDoContactInvitationCheck = false; - bool _inDoNewMessageCheck = false; - - int _contactInvitationCheckTick = 0; - int _newMessageCheckTick = 0; @override void initState() { @@ -73,145 +65,9 @@ class BackgroundTickerState extends State { _inTick = true; try { // Tick DHT record pool - if (!DHTRecordPool.instance.inTick) { - unawaited(DHTRecordPool.instance.tick()); - } - - // Check extant contact invitations once every N seconds - _contactInvitationCheckTick += 1; - if (_contactInvitationCheckTick >= ticksPerContactInvitationCheck) { - _contactInvitationCheckTick = 0; - if (!_inDoContactInvitationCheck) { - unawaited(_doContactInvitationCheck()); - } - } - - // Check new messages once every N seconds - _newMessageCheckTick += 1; - if (_newMessageCheckTick >= ticksPerNewMessageCheck) { - _newMessageCheckTick = 0; - if (!_inDoNewMessageCheck) { - unawaited(_doNewMessageCheck()); - } - } + unawaited(DHTRecordPool.instance.tick()); } finally { _inTick = false; } } - - Future _doContactInvitationCheck() async { - if (_inDoContactInvitationCheck) { - return; - } - _inDoContactInvitationCheck = true; - - if (!ProcessorRepository - .instance.processorConnectionState.isPublicInternetReady) { - return; - } - // final contactInvitationRecords = - // await ref.read(fetchContactInvitationRecordsProvider.future); - // if (contactInvitationRecords == null) { - // return; - // } - try { - // final activeAccountInfo = - // await ref.read(fetchActiveAccountProvider.future); - // if (activeAccountInfo == null) { - // return; - // } - - // final allChecks = >[]; - // for (final contactInvitationRecord in contactInvitationRecords) { - // allChecks.add(() async { - // final acceptReject = await checkAcceptRejectContact( - // activeAccountInfo: activeAccountInfo, - // contactInvitationRecord: contactInvitationRecord); - // if (acceptReject != null) { - // final acceptedContact = acceptReject.acceptedContact; - // if (acceptedContact != null) { - // // Accept - // await createContact( - // activeAccountInfo: activeAccountInfo, - // profile: acceptedContact.profile, - // remoteIdentity: acceptedContact.remoteIdentity, - // remoteConversationRecordKey: - // acceptedContact.remoteConversationRecordKey, - // localConversationRecordKey: - // acceptedContact.localConversationRecordKey, - // ); - // ref - // ..invalidate(fetchContactInvitationRecordsProvider) - // ..invalidate(fetchContactListProvider); - // } else { - // // Reject - // ref.invalidate(fetchContactInvitationRecordsProvider); - // } - // } - // }()); - // } - // await Future.wait(allChecks); - } finally { - _inDoContactInvitationCheck = true; - } - } - - Future _doNewMessageCheck() async { - if (_inDoNewMessageCheck) { - return; - } - _inDoNewMessageCheck = true; - - try { - if (!ProcessorRepository - .instance.processorConnectionState.isPublicInternetReady) { - return; - } - // final activeChat = ref.read(activeChatStateProvider); - // if (activeChat == null) { - // return; - // } - // final activeAccountInfo = - // await ref.read(fetchActiveAccountProvider.future); - // if (activeAccountInfo == null) { - // return; - // } - - // final contactList = ref.read(fetchContactListProvider).asData?.value ?? - // const IListConst([]); - - // final activeChatContactIdx = contactList.indexWhere( - // (c) => - // proto.TypedKeyProto.fromProto(c.remoteConversationRecordKey) == - // activeChat, - // ); - // if (activeChatContactIdx == -1) { - // return; - // } - // final activeChatContact = contactList[activeChatContactIdx]; - // final remoteIdentityPublicKey = - // proto.TypedKeyProto.fromProto(activeChatContact.identityPublicKey); - // final remoteConversationRecordKey = proto.TypedKeyProto.fromProto( - // activeChatContact.remoteConversationRecordKey); - // final localConversationRecordKey = proto.TypedKeyProto.fromProto( - // activeChatContact.localConversationRecordKey); - - // final newMessages = await getRemoteConversationMessages( - // activeAccountInfo: activeAccountInfo, - // remoteIdentityPublicKey: remoteIdentityPublicKey, - // remoteConversationRecordKey: remoteConversationRecordKey); - // if (newMessages != null && newMessages.isNotEmpty) { - // final changed = await mergeLocalConversationMessages( - // activeAccountInfo: activeAccountInfo, - // localConversationRecordKey: localConversationRecordKey, - // remoteIdentityPublicKey: remoteIdentityPublicKey, - // newMessages: newMessages); - // if (changed) { - // ref.invalidate(activeConversationMessagesProvider); - // } - // } - } finally { - _inDoNewMessageCheck = false; - } - } } diff --git a/lib/veilid_processor/views/developer.dart b/lib/veilid_processor/views/developer.dart index f738d6c..8154da5 100644 --- a/lib/veilid_processor/views/developer.dart +++ b/lib/veilid_processor/views/developer.dart @@ -29,10 +29,10 @@ class DeveloperPage extends StatefulWidget { const DeveloperPage({super.key}); @override - DeveloperPageState createState() => DeveloperPageState(); + State createState() => _DeveloperPageState(); } -class DeveloperPageState extends State { +class _DeveloperPageState extends State { final _terminalController = TerminalController(); final _debugCommandController = TextEditingController(); final _logLevelController = DropdownController(duration: 250.ms); @@ -43,6 +43,12 @@ class DeveloperPageState extends State { @override void initState() { super.initState(); + + WidgetsBinding.instance.addPostFrameCallback((_) async { + await changeWindowSetup( + TitleBarStyle.normal, OrientationCapability.normal); + }); + _terminalController.addListener(() { setState(() {}); }); diff --git a/packages/bloc_tools/lib/src/state_follower.dart b/packages/bloc_tools/lib/src/state_follower.dart index cc955b3..eebe27b 100644 --- a/packages/bloc_tools/lib/src/state_follower.dart +++ b/packages/bloc_tools/lib/src/state_follower.dart @@ -57,7 +57,7 @@ abstract mixin class StateFollower { } late IMap _lastInputStateMap; + late final StreamSubscription _subscription; final SingleStateProcessor> _singleStateProcessor = SingleStateProcessor(); - late final StreamSubscription _subscription; } diff --git a/packages/veilid_support/lib/dht_support/src/dht_record_pool.dart b/packages/veilid_support/lib/dht_support/src/dht_record_pool.dart index bb835d1..d823038 100644 --- a/packages/veilid_support/lib/dht_support/src/dht_record_pool.dart +++ b/packages/veilid_support/lib/dht_support/src/dht_record_pool.dart @@ -15,6 +15,9 @@ part 'dht_record_pool.g.dart'; part 'dht_record.dart'; +const int watchBackoffMultiplier = 2; +const int watchBackoffMax = 30; + /// Record pool that managed DHTRecords and allows for tagged deletion @freezed class DHTRecordPoolAllocations with _$DHTRecordPoolAllocations { @@ -109,7 +112,11 @@ class DHTRecordPool with TableDBBacked { // Convenience accessor final Veilid _veilid; // If tick is already running or not - bool inTick = false; + bool _inTick = false; + // Tick counter for backoff + int _tickCount = 0; + // Backoff timer + int _watchBackoffTimer = 1; static DHTRecordPool? _singleton; @@ -578,13 +585,19 @@ class DHTRecordPool with TableDBBacked { /// Ticker to check watch state change requests Future tick() async { - if (inTick) { + if (_tickCount < _watchBackoffTimer) { + _tickCount++; return; } - inTick = true; + if (_inTick) { + return; + } + _inTick = true; + _tickCount = 0; + try { // See if any opened records need watch state changes - final unord = Function()>[]; + final unord = Function()>[]; await _mutex.protect(() async { for (final kv in _opened.entries) { @@ -600,16 +613,19 @@ class DHTRecordPool with TableDBBacked { if (watchState == null) { unord.add(() async { // Record needs watch cancel + var success = false; try { - await dhtctx.cancelDHTWatch(openedRecordKey); + success = await dhtctx.cancelDHTWatch(openedRecordKey); openedRecordInfo.shared.needsWatchStateUpdate = false; } on VeilidAPIException { // Failed to cancel DHT watch, try again next tick } + return success; }); } else { unord.add(() async { // Record needs new watch + var success = false; try { final realExpiration = await dhtctx.watchDHTValues( openedRecordKey, @@ -622,10 +638,12 @@ class DHTRecordPool with TableDBBacked { openedRecordInfo.shared.needsWatchStateUpdate = false; _updateWatchExpirations( openedRecordInfo.records, realExpiration); + success = true; } } on VeilidAPIException { // Failed to cancel DHT watch, try again next tick } + return success; }); } } @@ -633,9 +651,18 @@ class DHTRecordPool with TableDBBacked { }); // Process all watch changes - await unord.map((f) => f()).wait; + // If any watched did not success, back off the attempts to + // update the watches for a bit + final allSuccess = + (await unord.map((f) => f()).wait).reduce((a, b) => a && b); + if (!allSuccess) { + _watchBackoffTimer *= watchBackoffMultiplier; + _watchBackoffTimer = min(_watchBackoffTimer, watchBackoffMax); + } else { + _watchBackoffTimer = 1; + } } finally { - inTick = false; + _inTick = false; } } } From 56d8f81cf26e3595cc54f973c232aad7b7aea440 Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Wed, 28 Feb 2024 22:41:44 -0500 Subject: [PATCH 53/68] fix exception --- .../veilid_support/lib/dht_support/src/dht_record_pool.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/veilid_support/lib/dht_support/src/dht_record_pool.dart b/packages/veilid_support/lib/dht_support/src/dht_record_pool.dart index d823038..7389123 100644 --- a/packages/veilid_support/lib/dht_support/src/dht_record_pool.dart +++ b/packages/veilid_support/lib/dht_support/src/dht_record_pool.dart @@ -653,7 +653,7 @@ class DHTRecordPool with TableDBBacked { // Process all watch changes // If any watched did not success, back off the attempts to // update the watches for a bit - final allSuccess = + final allSuccess = unord.isEmpty || (await unord.map((f) => f()).wait).reduce((a, b) => a && b); if (!allSuccess) { _watchBackoffTimer *= watchBackoffMultiplier; From 0cf2b947beb5d28220b153c7fc90ef52a39285a1 Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Thu, 29 Feb 2024 13:54:03 -0500 Subject: [PATCH 54/68] fix refresh, error --- .../views/contact_invitation_display.dart | 14 ++-- .../lib/dht_support/src/dht_short_array.dart | 37 +++++++++ .../src/dht_short_array_cubit.dart | 79 +++++++++---------- pubspec.lock | 16 +--- pubspec.yaml | 2 +- 5 files changed, 86 insertions(+), 62 deletions(-) diff --git a/lib/contact_invitation/views/contact_invitation_display.dart b/lib/contact_invitation/views/contact_invitation_display.dart index fe2ed33..4a1a7c2 100644 --- a/lib/contact_invitation/views/contact_invitation_display.dart +++ b/lib/contact_invitation/views/contact_invitation_display.dart @@ -26,8 +26,8 @@ class ContactInvitationDisplayDialog extends StatefulWidget { final String message; @override - ContactInvitationDisplayDialogState createState() => - ContactInvitationDisplayDialogState(); + State createState() => + _ContactInvitationDisplayDialogState(); @override void debugFillProperties(DiagnosticPropertiesBuilder properties) { @@ -36,7 +36,7 @@ class ContactInvitationDisplayDialog extends StatefulWidget { } } -class ContactInvitationDisplayDialogState +class _ContactInvitationDisplayDialogState extends State { final focusNode = FocusNode(); final formKey = GlobalKey(); @@ -123,12 +123,8 @@ class ContactInvitationDisplayDialogState }, ).paddingAll(16), ])), - error: (e, s) { - Navigator.of(context).pop(); - showErrorToast(context, - translate('send_invite_dialog.failed_to_generate')); - return const Text(''); - }))); + error: (e, s) => + Text(translate('send_invite_dialog.failed_to_generate'))))); } @override diff --git a/packages/veilid_support/lib/dht_support/src/dht_short_array.dart b/packages/veilid_support/lib/dht_support/src/dht_short_array.dart index 09bd2d7..8cc0e37 100644 --- a/packages/veilid_support/lib/dht_support/src/dht_short_array.dart +++ b/packages/veilid_support/lib/dht_support/src/dht_short_array.dart @@ -212,17 +212,54 @@ class DHTShortArray { return record!.get(subkey: recordSubkey, forceRefresh: forceRefresh); } + Future?> getAllItems({bool forceRefresh = false}) async { + await _refreshHead(forceRefresh: forceRefresh, onlyUpdates: true); + + final out = []; + + for (var pos = 0; pos < _head.index.length; pos++) { + final index = _head.index[pos]; + final recordNumber = index ~/ _stride; + final record = _getLinkedRecord(recordNumber); + if (record == null) { + assert(record != null, 'Record does not exist'); + return null; + } + + final recordSubkey = (index % _stride) + ((recordNumber == 0) ? 1 : 0); + final elem = + await record.get(subkey: recordSubkey, forceRefresh: forceRefresh); + if (elem == null) { + return null; + } + out.add(elem); + } + + return out; + } + Future getItemJson(T Function(dynamic) fromJson, int pos, {bool forceRefresh = false}) => getItem(pos, forceRefresh: forceRefresh) .then((out) => jsonDecodeOptBytes(fromJson, out)); + Future?> getAllItemsJson(T Function(dynamic) fromJson, + {bool forceRefresh = false}) => + getAllItems(forceRefresh: forceRefresh) + .then((out) => out?.map(fromJson).toList()); + Future getItemProtobuf( T Function(List) fromBuffer, int pos, {bool forceRefresh = false}) => getItem(pos, forceRefresh: forceRefresh) .then((out) => (out == null) ? null : fromBuffer(out)); + Future?> getAllItemsProtobuf( + T Function(List) fromBuffer, + {bool forceRefresh = false}) => + getAllItems(forceRefresh: forceRefresh) + .then((out) => out?.map(fromBuffer).toList()); + Future tryAddItem(Uint8List value) async { await _refreshHead(onlyUpdates: true); diff --git a/packages/veilid_support/lib/dht_support/src/dht_short_array_cubit.dart b/packages/veilid_support/lib/dht_support/src/dht_short_array_cubit.dart index 8309254..b8920cd 100644 --- a/packages/veilid_support/lib/dht_support/src/dht_short_array_cubit.dart +++ b/packages/veilid_support/lib/dht_support/src/dht_short_array_cubit.dart @@ -18,13 +18,13 @@ class DHTShortArrayCubit extends Cubit> required T Function(List data) decodeElement, }) : _decodeElement = decodeElement, super(const BlocBusyState(AsyncValue.loading())) { - Future.delayed(Duration.zero, () async { + _initFuture = Future(() async { // Open DHT record _shortArray = await open(); _wantsCloseRecord = true; // Make initial state update - _update(); + await _refreshNoWait(); _subscription = await _shortArray.listen(_update); }); } @@ -35,57 +35,53 @@ class DHTShortArrayCubit extends Cubit> }) : _shortArray = shortArray, _decodeElement = decodeElement, super(const BlocBusyState(AsyncValue.loading())) { - // Make initial state update - _update(); - Future.delayed(Duration.zero, () async { + _initFuture = Future(() async { + // Make initial state update + await _refreshNoWait(); _subscription = await shortArray.listen(_update); }); } - Future refresh({bool forceRefresh = false}) async => busy((emit) async { - var out = IList(); - // xxx could be parallelized but we need to watch out for rate limits - for (var i = 0; i < _shortArray.length; i++) { - final cir = await _shortArray.getItem(i, forceRefresh: forceRefresh); - if (cir == null) { - throw Exception('Failed to get short array element'); - } - out = out.add(_decodeElement(cir)); - } - emit(AsyncValue.data(out)); + Future refresh({bool forceRefresh = false}) async { + await _initFuture; + await _refreshNoWait(forceRefresh: forceRefresh); + } + + Future _refreshNoWait({bool forceRefresh = false}) async => + busy((emit) async { + await _refreshInner(emit, forceRefresh: forceRefresh); }); + Future _refreshInner(void Function(AsyncValue>) emit, + {bool forceRefresh = false}) async { + try { + final newState = + (await _shortArray.getAllItems(forceRefresh: forceRefresh)) + ?.map(_decodeElement) + .toIList(); + if (newState == null) { + emit(const AsyncValue.loading()); + } else { + emit(AsyncValue.data(newState)); + } + } on Exception catch (e) { + emit(AsyncValue.error(e)); + } + } + void _update() { // Run at most one background update process // Because this is async, we could get an update while we're - // still processing the last one + // still processing the last one. Only called after init future has run + // so we dont have to wait for that here. _sspUpdate.busyUpdate>>(busy, (emit) async { - try { - final initialState = await _getElementsInner(); - emit(AsyncValue.data(initialState)); - } on Exception catch (e) { - emit(AsyncValue.error(e)); - } + await _refreshInner(emit); }); } - // Get and decode the entire short array - Future> _getElementsInner() async { - assert(isBusy, 'should only be called from a busy state'); - var out = IList(); - for (var i = 0; i < _shortArray.length; i++) { - // Get the element bytes (throw if fails, array state is invalid) - final bytes = (await _shortArray.getItem(i))!; - // Decode the element - final elem = _decodeElement(bytes); - // Append to the output list - out = out.add(elem); - } - return out; - } - @override Future close() async { + await _initFuture; await _subscription?.cancel(); _subscription = null; if (_wantsCloseRecord) { @@ -94,10 +90,13 @@ class DHTShortArrayCubit extends Cubit> await super.close(); } - Future operate(Future Function(DHTShortArray) closure) async => - _operateMutex.protect(() async => closure(_shortArray)); + Future operate(Future Function(DHTShortArray) closure) async { + await _initFuture; + return _operateMutex.protect(() async => closure(_shortArray)); + } final _operateMutex = Mutex(); + late final Future _initFuture; late final DHTShortArray _shortArray; final T Function(List data) _decodeElement; StreamSubscription? _subscription; diff --git a/pubspec.lock b/pubspec.lock index e6de13e..2a2bec9 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -251,10 +251,10 @@ packages: dependency: "direct main" description: name: change_case - sha256: f4e08feaa845e75e4f5ad2b0e15f24813d7ea6c27e7b78252f0c17f752cf1157 + sha256: "47c48c36f95f20c6d0ba03efabceff261d05026cca322cc2c4c01c343371b5bb" url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "2.0.1" characters: dependency: transitive description: @@ -423,14 +423,6 @@ packages: url: "https://pub.dev" source: hosted version: "7.0.0" - file_utils: - dependency: transitive - description: - name: file_utils - sha256: d1e64389a22649095c8405c9e177272caf05139255931c9ff30d53b5c9bcaa34 - url: "https://pub.dev" - source: hosted - version: "1.0.1" fixnum: dependency: "direct main" description: @@ -1342,10 +1334,10 @@ packages: dependency: transitive description: name: system_info2 - sha256: af2f948e3f31a3367a049932a8ad59faf0063ecf836a020d975b9f41566d8bc9 + sha256: "65206bbef475217008b5827374767550a5420ce70a04d2d7e94d1d2253f3efc9" url: "https://pub.dev" source: hosted - version: "3.0.2" + version: "4.0.0" system_info_plus: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index e25b649..8b2341d 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -20,7 +20,7 @@ dependencies: bloc_tools: path: packages/bloc_tools blurry_modal_progress_hud: ^1.1.1 - change_case: ^1.1.0 + change_case: ^2.0.1 charcode: ^1.3.1 circular_profile_avatar: ^2.0.5 circular_reveal_animation: ^2.0.1 From ce4601b575b12aae24adcc2a70dc33dabcf92bfe Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Thu, 29 Feb 2024 14:37:50 -0500 Subject: [PATCH 55/68] clean up mounts --- lib/chat/views/chat_component.dart | 48 +++++++++---------- .../views/contact_invitation_display.dart | 3 +- .../views/contact_invitation_item_widget.dart | 1 - .../views/send_invite_dialog.dart | 21 +++----- 4 files changed, 31 insertions(+), 42 deletions(-) diff --git a/lib/chat/views/chat_component.dart b/lib/chat/views/chat_component.dart index 03a05ba..decf840 100644 --- a/lib/chat/views/chat_component.dart +++ b/lib/chat/views/chat_component.dart @@ -2,7 +2,6 @@ import 'dart:async'; import 'package:async_tools/async_tools.dart'; import 'package:awesome_extensions/awesome_extensions.dart'; -import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_chat_types/flutter_chat_types.dart' as types; @@ -21,19 +20,19 @@ class ChatComponent extends StatelessWidget { const ChatComponent._( {required TypedKey localUserIdentityKey, required TypedKey remoteConversationRecordKey, - required IList messages, + required MessagesCubit messagesCubit, required types.User localUser, required types.User remoteUser, super.key}) : _localUserIdentityKey = localUserIdentityKey, _remoteConversationRecordKey = remoteConversationRecordKey, - _messages = messages, + _messagesCubit = messagesCubit, _localUser = localUser, _remoteUser = remoteUser; final TypedKey _localUserIdentityKey; final TypedKey _remoteConversationRecordKey; - final IList _messages; + final MessagesCubit _messagesCubit; final types.User _localUser; final types.User _remoteUser; @@ -78,24 +77,22 @@ class ChatComponent extends StatelessWidget { id: conversation.contact.identityPublicKey.toVeilid().toString(), firstName: editedName); + // Get the messages cubit + final messagesCubit = context + .select( + (x) => x.tryOperate(remoteConversationRecordKey, + closure: (cubit) => cubit)); + // Get the messages to display // and ensure it is safe to operate() on the MessageCubit for this chat - final avmessages = context.select< - ActiveConversationMessagesBlocMapCubit, - AsyncValue>?>( - (x) => x.state[remoteConversationRecordKey]); - if (avmessages == null) { + if (messagesCubit == null) { return waitingPage(); } - final messages = avmessages.data?.value; - if (messages == null) { - return avmessages.buildNotData(); - } return ChatComponent._( localUserIdentityKey: localUserIdentityKey, remoteConversationRecordKey: remoteConversationRecordKey, - messages: messages, + messagesCubit: messagesCubit, localUser: localUser, remoteUser: remoteUser, key: key); @@ -115,24 +112,21 @@ class ChatComponent extends StatelessWidget { return textMessage; } - Future _addMessage(BuildContext context, proto.Message message) async { + Future _addMessage(proto.Message message) async { if (message.text.isEmpty) { return; } - await context.read().operate( - _remoteConversationRecordKey, - closure: (messagesCubit) => messagesCubit.addMessage(message: message)); + await _messagesCubit.addMessage(message: message); } - Future _handleSendPressed( - BuildContext context, types.PartialText message) async { + Future _handleSendPressed(types.PartialText message) async { final protoMessage = proto.Message() ..author = _localUserIdentityKey.toProto() ..timestamp = Veilid.instance.now().toInt64() ..text = message.text; //..signature = signature; - await _addMessage(context, protoMessage); + await _addMessage(protoMessage); } Future _handleAttachmentPressed() async { @@ -146,9 +140,15 @@ class ChatComponent extends StatelessWidget { final textTheme = Theme.of(context).textTheme; final chatTheme = makeChatTheme(scale, textTheme); + final avmessages = _messagesCubit.state; + final messages = avmessages.data?.value; + if (messages == null) { + return avmessages.buildNotData(); + } + // Convert protobuf messages to chat messages final chatMessages = []; - for (final message in _messages) { + for (final message in messages) { final chatMessage = messageToChatMessage(message); chatMessages.insert(0, chatMessage); } @@ -194,8 +194,8 @@ class ChatComponent extends StatelessWidget { //onMessageTap: _handleMessageTap, //onPreviewDataFetched: _handlePreviewDataFetched, onSendPressed: (message) { - singleFuture(this, - () async => _handleSendPressed(context, message)); + singleFuture( + this, () async => _handleSendPressed(message)); }, //showUserAvatars: false, //showUserNames: true, diff --git a/lib/contact_invitation/views/contact_invitation_display.dart b/lib/contact_invitation/views/contact_invitation_display.dart index 4a1a7c2..33036e7 100644 --- a/lib/contact_invitation/views/contact_invitation_display.dart +++ b/lib/contact_invitation/views/contact_invitation_display.dart @@ -123,8 +123,7 @@ class _ContactInvitationDisplayDialogState }, ).paddingAll(16), ])), - error: (e, s) => - Text(translate('send_invite_dialog.failed_to_generate'))))); + error: errorPage))); } @override diff --git a/lib/contact_invitation/views/contact_invitation_item_widget.dart b/lib/contact_invitation/views/contact_invitation_item_widget.dart index 2ed25c5..fee8629 100644 --- a/lib/contact_invitation/views/contact_invitation_item_widget.dart +++ b/lib/contact_invitation/views/contact_invitation_item_widget.dart @@ -102,7 +102,6 @@ class ContactInvitationItemWidget extends StatelessWidget { onTap: disabled ? null : () async { - // ignore: use_build_context_synchronously if (!context.mounted) { return; } diff --git a/lib/contact_invitation/views/send_invite_dialog.dart b/lib/contact_invitation/views/send_invite_dialog.dart index 8113078..fb83254 100644 --- a/lib/contact_invitation/views/send_invite_dialog.dart +++ b/lib/contact_invitation/views/send_invite_dialog.dart @@ -66,8 +66,7 @@ class SendInviteDialogState extends State { if (pin == null) { return; } - // ignore: use_build_context_synchronously - if (!context.mounted) { + if (!mounted) { return; } final matchpin = await showDialog( @@ -84,8 +83,7 @@ class SendInviteDialogState extends State { _encryptionKey = pin; }); } else { - // ignore: use_build_context_synchronously - if (!context.mounted) { + if (!mounted) { return; } showErrorToast( @@ -105,8 +103,7 @@ class SendInviteDialogState extends State { if (password == null) { return; } - // ignore: use_build_context_synchronously - if (!context.mounted) { + if (!mounted) { return; } final matchpass = await showDialog( @@ -123,8 +120,7 @@ class SendInviteDialogState extends State { _encryptionKey = password; }); } else { - // ignore: use_build_context_synchronously - if (!context.mounted) { + if (!mounted) { return; } showErrorToast( @@ -148,10 +144,7 @@ class SendInviteDialogState extends State { encryptionKey: _encryptionKey, message: _messageTextController.text, expiration: _expiration); - // ignore: use_build_context_synchronously - if (!context.mounted) { - return; - } + await showDialog( context: context, builder: (context) => BlocProvider( @@ -159,9 +152,7 @@ class SendInviteDialogState extends State { child: ContactInvitationDisplayDialog( message: _messageTextController.text, ))); - // if (ret == null) { - // return; - // } + navigator.pop(); } From f896fc822c5fd2ac36f9dab1faec404635f19dd0 Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Thu, 29 Feb 2024 21:21:20 -0500 Subject: [PATCH 56/68] not much better, some race condition exists. figuring that out. --- .../cubits/contact_invitation_list_cubit.dart | 4 ++-- .../lib/dht_support/src/dht_short_array_cubit.dart | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/contact_invitation/cubits/contact_invitation_list_cubit.dart b/lib/contact_invitation/cubits/contact_invitation_list_cubit.dart index 399fafa..4ffd2bd 100644 --- a/lib/contact_invitation/cubits/contact_invitation_list_cubit.dart +++ b/lib/contact_invitation/cubits/contact_invitation_list_cubit.dart @@ -41,11 +41,11 @@ class ContactInvitationListCubit final accountRecordKey = activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey; - final contactInvitationListRecordKey = + final contactInvitationListRecordPointer = account.contactInvitationRecords.toVeilid(); final dhtRecord = await DHTShortArray.openOwned( - contactInvitationListRecordKey, + contactInvitationListRecordPointer, parent: accountRecordKey); return dhtRecord; diff --git a/packages/veilid_support/lib/dht_support/src/dht_short_array_cubit.dart b/packages/veilid_support/lib/dht_support/src/dht_short_array_cubit.dart index b8920cd..7937b12 100644 --- a/packages/veilid_support/lib/dht_support/src/dht_short_array_cubit.dart +++ b/packages/veilid_support/lib/dht_support/src/dht_short_array_cubit.dart @@ -24,7 +24,7 @@ class DHTShortArrayCubit extends Cubit> _wantsCloseRecord = true; // Make initial state update - await _refreshNoWait(); + unawaited(_refreshNoWait()); _subscription = await _shortArray.listen(_update); }); } @@ -37,7 +37,7 @@ class DHTShortArrayCubit extends Cubit> super(const BlocBusyState(AsyncValue.loading())) { _initFuture = Future(() async { // Make initial state update - await _refreshNoWait(); + unawaited(_refreshNoWait()); _subscription = await shortArray.listen(_update); }); } From 2cf1c5f4e96ec25353fea6a388dc33412ec5bc43 Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Fri, 1 Mar 2024 16:25:56 -0500 Subject: [PATCH 57/68] better logging --- .../account_repository.dart | 9 +- .../new_account_page/new_account_page.dart | 2 +- lib/chat/cubits/messages_cubit.dart | 55 +++---- packages/veilid_support/lib/src/config.dart | 33 ++-- packages/veilid_support/lib/src/identity.dart | 148 +++++++++--------- .../veilid_support/lib/src/veilid_log.dart | 14 +- .../veilid_support/lib/veilid_support.dart | 2 +- pubspec.lock | 2 +- pubspec.yaml | 2 +- 9 files changed, 147 insertions(+), 120 deletions(-) diff --git a/lib/account_manager/repository/account_repository/account_repository.dart b/lib/account_manager/repository/account_repository/account_repository.dart index 872393e..1b145b3 100644 --- a/lib/account_manager/repository/account_repository/account_repository.dart +++ b/lib/account_manager/repository/account_repository/account_repository.dart @@ -178,7 +178,9 @@ class AccountRepository { /// Creates a new master identity, an account associated with the master /// identity, stores the account in the identity key and then logs into /// that account with no password set at this time - Future createMasterIdentity(NewProfileSpec newProfileSpec) async { + Future createWithNewMasterIdentity( + NewProfileSpec newProfileSpec) async { + log.debug('Creating master identity'); final imws = await IdentityMasterWithSecrets.create(); try { final localAccount = await _newLocalAccount( @@ -204,6 +206,8 @@ class AccountRepository { required NewProfileSpec newProfileSpec, EncryptionKeyType encryptionKeyType = EncryptionKeyType.none, String encryptionKey = ''}) async { + log.debug('Creating new local account'); + final localAccounts = await _localAccounts.get(); // Add account with profile to DHT @@ -212,15 +216,18 @@ class AccountRepository { accountKey: veilidChatAccountKey, createAccountCallback: (parent) async { // Make empty contact list + log.debug('Creating contacts list'); final contactList = await (await DHTShortArray.create(parent: parent)) .scope((r) async => r.record.ownedDHTRecordPointer); // Make empty contact invitation record list + log.debug('Creating contact invitation records list'); final contactInvitationRecords = await (await DHTShortArray.create(parent: parent)) .scope((r) async => r.record.ownedDHTRecordPointer); // Make empty chat record list + log.debug('Creating chat records list'); final chatRecords = await (await DHTShortArray.create(parent: parent)) .scope((r) async => r.record.ownedDHTRecordPointer); diff --git a/lib/account_manager/views/new_account_page/new_account_page.dart b/lib/account_manager/views/new_account_page/new_account_page.dart index b57c5fa..b249a99 100644 --- a/lib/account_manager/views/new_account_page/new_account_page.dart +++ b/lib/account_manager/views/new_account_page/new_account_page.dart @@ -122,7 +122,7 @@ class NewAccountPageState extends State { NewProfileSpec(name: name, pronouns: pronouns); await AccountRepository.instance - .createMasterIdentity(newProfileSpec); + .createWithNewMasterIdentity(newProfileSpec); } on Exception catch (e) { if (context.mounted) { await showErrorModal(context, translate('new_account_page.error'), diff --git a/lib/chat/cubits/messages_cubit.dart b/lib/chat/cubits/messages_cubit.dart index c1528de..ee259dd 100644 --- a/lib/chat/cubits/messages_cubit.dart +++ b/lib/chat/cubits/messages_cubit.dart @@ -25,29 +25,20 @@ class MessagesCubit extends Cubit>> { required TypedKey remoteConversationRecordKey, required TypedKey remoteMessagesRecordKey}) : _activeAccountInfo = activeAccountInfo, - _localMessagesRecordKey = localMessagesRecordKey, _remoteIdentityPublicKey = remoteIdentityPublicKey, - _remoteMessagesRecordKey = remoteMessagesRecordKey, _remoteMessagesQueue = StreamController(), super(const AsyncValue.loading()) { // Local messages key - Future.delayed(Duration.zero, () async { - final crypto = await getMessagesCrypto(); - final writer = _activeAccountInfo.conversationWriter; - final record = await DHTShortArray.openWrite( - _localMessagesRecordKey, writer, - parent: localConversationRecordKey, crypto: crypto); - await _setLocalMessages(record); - }); + Future.delayed( + Duration.zero, + () async => _initLocalMessages( + localConversationRecordKey, localMessagesRecordKey)); // Remote messages key - Future.delayed(Duration.zero, () async { - // Open remote record key if it is specified - final crypto = await getMessagesCrypto(); - final record = await DHTShortArray.openRead(_remoteMessagesRecordKey, - parent: remoteConversationRecordKey, crypto: crypto); - await _setRemoteMessages(record); - }); + Future.delayed( + Duration.zero, + () async => _initRemoteMessages( + remoteConversationRecordKey, remoteMessagesRecordKey)); // Remote messages listener Future.delayed(Duration.zero, () async { @@ -59,8 +50,11 @@ class MessagesCubit extends Cubit>> { @override Future close() async { + await _remoteMessagesQueue.close(); await _localSubscription?.cancel(); await _remoteSubscription?.cancel(); + await _localMessagesCubit?.close(); + await _remoteMessagesCubit?.close(); await super.close(); } @@ -129,20 +123,29 @@ class MessagesCubit extends Cubit>> { } // Open local messages key - Future _setLocalMessages(DHTShortArray localMessagesRecord) async { - assert(_localMessagesCubit == null, 'shoud not set local messages twice'); - _localMessagesCubit = DHTShortArrayCubit.value( - shortArray: localMessagesRecord, + Future _initLocalMessages(TypedKey localConversationRecordKey, + TypedKey localMessagesRecordKey) async { + final crypto = await getMessagesCrypto(); + final writer = _activeAccountInfo.conversationWriter; + + _localMessagesCubit = DHTShortArrayCubit( + open: () async => DHTShortArray.openWrite( + localMessagesRecordKey, writer, + parent: localConversationRecordKey, crypto: crypto), decodeElement: proto.Message.fromBuffer); _localSubscription = _localMessagesCubit!.stream.listen(updateLocalMessagesState); } // Open remote messages key - Future _setRemoteMessages(DHTShortArray remoteMessagesRecord) async { - assert(_remoteMessagesCubit == null, 'shoud not set remote messages twice'); - _remoteMessagesCubit = DHTShortArrayCubit.value( - shortArray: remoteMessagesRecord, + Future _initRemoteMessages(TypedKey remoteConversationRecordKey, + TypedKey remoteMessagesRecordKey) async { + // Open remote record key if it is specified + final crypto = await getMessagesCrypto(); + + _remoteMessagesCubit = DHTShortArrayCubit( + open: () async => DHTShortArray.openRead(remoteMessagesRecordKey, + parent: remoteConversationRecordKey, crypto: crypto), decodeElement: proto.Message.fromBuffer); _remoteSubscription = _remoteMessagesCubit!.stream.listen(updateRemoteMessagesState); @@ -208,8 +211,6 @@ class MessagesCubit extends Cubit>> { final ActiveAccountInfo _activeAccountInfo; final TypedKey _remoteIdentityPublicKey; - final TypedKey _localMessagesRecordKey; - final TypedKey _remoteMessagesRecordKey; DHTShortArrayCubit? _localMessagesCubit; DHTShortArrayCubit? _remoteMessagesCubit; final StreamController<_MessageQueueEntry> _remoteMessagesQueue; diff --git a/packages/veilid_support/lib/src/config.dart b/packages/veilid_support/lib/src/config.dart index 1d2b521..dff9f3e 100644 --- a/packages/veilid_support/lib/src/config.dart +++ b/packages/veilid_support/lib/src/config.dart @@ -2,31 +2,44 @@ import 'package:veilid/veilid.dart'; Map getDefaultVeilidPlatformConfig( bool isWeb, String appName) { + final ignoreLogTargetsStr = + // ignore: do_not_use_environment + const String.fromEnvironment('IGNORE_LOG_TARGETS').trim(); + final ignoreLogTargets = ignoreLogTargetsStr.isEmpty + ? [] + : ignoreLogTargetsStr.split(',').map((e) => e.trim()).toList(); + if (isWeb) { - return const VeilidWASMConfig( + return VeilidWASMConfig( logging: VeilidWASMConfigLogging( performance: VeilidWASMConfigLoggingPerformance( enabled: true, level: VeilidConfigLogLevel.debug, logsInTimings: true, - logsInConsole: false), + logsInConsole: false, + ignoreLogTargets: ignoreLogTargets), api: VeilidWASMConfigLoggingApi( - enabled: true, level: VeilidConfigLogLevel.info))) + enabled: true, + level: VeilidConfigLogLevel.info, + ignoreLogTargets: ignoreLogTargets))) .toJson(); } return VeilidFFIConfig( logging: VeilidFFIConfigLogging( - terminal: const VeilidFFIConfigLoggingTerminal( - enabled: false, - level: VeilidConfigLogLevel.debug, - ), + terminal: VeilidFFIConfigLoggingTerminal( + enabled: false, + level: VeilidConfigLogLevel.debug, + ignoreLogTargets: ignoreLogTargets), otlp: VeilidFFIConfigLoggingOtlp( enabled: false, level: VeilidConfigLogLevel.trace, grpcEndpoint: '127.0.0.1:4317', - serviceName: appName), - api: const VeilidFFIConfigLoggingApi( - enabled: true, level: VeilidConfigLogLevel.info))) + serviceName: appName, + ignoreLogTargets: ignoreLogTargets), + api: VeilidFFIConfigLoggingApi( + enabled: true, + level: VeilidConfigLogLevel.info, + ignoreLogTargets: ignoreLogTargets))) .toJson(); } diff --git a/packages/veilid_support/lib/src/identity.dart b/packages/veilid_support/lib/src/identity.dart index 70dc295..63361c7 100644 --- a/packages/veilid_support/lib/src/identity.dart +++ b/packages/veilid_support/lib/src/identity.dart @@ -3,9 +3,9 @@ import 'dart:typed_data'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:protobuf/protobuf.dart'; -import 'package:veilid/veilid.dart'; -import '../dht_support/dht_support.dart'; +import '../veilid_support.dart'; +import 'veilid_log.dart'; part 'identity.freezed.dart'; part 'identity.g.dart'; @@ -161,42 +161,44 @@ extension IdentityMasterExtension on IdentityMaster { /////// Add account with profile to DHT // Open identity key for writing + veilidLoggy.debug('Opening master identity'); return (await pool.openWrite( identityRecordKey, identityWriter(identitySecret), parent: masterRecordKey)) - .scope((identityRec) async => - // Create new account to insert into identity - (await pool.create(parent: identityRec.key)) - .deleteScope((accountRec) async { - final account = await createAccountCallback(accountRec.key); - // Write account key - await accountRec.eventualWriteProtobuf(account); + .scope((identityRec) async { + // Create new account to insert into identity + veilidLoggy.debug('Creating new account'); + return (await pool.create(parent: identityRec.key)) + .deleteScope((accountRec) async { + final account = await createAccountCallback(accountRec.key); + // Write account key + veilidLoggy.debug('Writing account record'); + await accountRec.eventualWriteProtobuf(account); - // Update identity key to include account - final newAccountRecordInfo = AccountRecordInfo( - accountRecord: OwnedDHTRecordPointer( - recordKey: accountRec.key, - owner: accountRec.ownerKeyPair!)); + // Update identity key to include account + final newAccountRecordInfo = AccountRecordInfo( + accountRecord: OwnedDHTRecordPointer( + recordKey: accountRec.key, owner: accountRec.ownerKeyPair!)); - await identityRec.eventualUpdateJson(Identity.fromJson, - (oldIdentity) async { - if (oldIdentity == null) { - throw IdentityException.readError; - } - final oldAccountRecords = - IMapOfSets.from(oldIdentity.accountRecords); + veilidLoggy.debug('Updating identity with new account'); + await identityRec.eventualUpdateJson(Identity.fromJson, + (oldIdentity) async { + if (oldIdentity == null) { + throw IdentityException.readError; + } + final oldAccountRecords = IMapOfSets.from(oldIdentity.accountRecords); - if (oldAccountRecords.get(accountKey).length >= maxAccounts) { - throw IdentityException.limitExceeded; - } - final accountRecords = oldAccountRecords - .add(accountKey, newAccountRecordInfo) - .asIMap(); - return oldIdentity.copyWith(accountRecords: accountRecords); - }); + if (oldAccountRecords.get(accountKey).length >= maxAccounts) { + throw IdentityException.limitExceeded; + } + final accountRecords = + oldAccountRecords.add(accountKey, newAccountRecordInfo).asIMap(); + return oldIdentity.copyWith(accountRecords: accountRecords); + }); - return newAccountRecordInfo; - })); + return newAccountRecordInfo; + }); + }); } } @@ -219,56 +221,58 @@ class IdentityMasterWithSecrets { final pool = DHTRecordPool.instance; // IdentityMaster DHT record is public/unencrypted + veilidLoggy.debug('Creating master identity record'); return (await pool.create(crypto: const DHTRecordCryptoPublic())) - .deleteScope((masterRec) async => - // Identity record is private - (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()); + .deleteScope((masterRec) async { + veilidLoggy.debug('Creating identity record'); + // Identity record is private + 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); + }); + }); } } diff --git a/packages/veilid_support/lib/src/veilid_log.dart b/packages/veilid_support/lib/src/veilid_log.dart index f5db930..fbabc70 100644 --- a/packages/veilid_support/lib/src/veilid_log.dart +++ b/packages/veilid_support/lib/src/veilid_log.dart @@ -1,4 +1,5 @@ import 'package:loggy/loggy.dart'; +import 'package:meta/meta.dart'; import 'package:veilid/veilid.dart'; // Loggy tools @@ -37,7 +38,8 @@ class VeilidLoggy implements LoggyType { Loggy get loggy => Loggy('Veilid'); } -Loggy get _veilidLoggy => Loggy('Veilid'); +@internal +Loggy get veilidLoggy => Loggy('Veilid'); void processLog(VeilidLog log) { StackTrace? stackTrace; @@ -50,19 +52,19 @@ void processLog(VeilidLog log) { switch (log.logLevel) { case VeilidLogLevel.error: - _veilidLoggy.error(log.message, error, stackTrace); + veilidLoggy.error(log.message, error, stackTrace); break; case VeilidLogLevel.warn: - _veilidLoggy.warning(log.message, error, stackTrace); + veilidLoggy.warning(log.message, error, stackTrace); break; case VeilidLogLevel.info: - _veilidLoggy.info(log.message, error, stackTrace); + veilidLoggy.info(log.message, error, stackTrace); break; case VeilidLogLevel.debug: - _veilidLoggy.debug(log.message, error, stackTrace); + veilidLoggy.debug(log.message, error, stackTrace); break; case VeilidLogLevel.trace: - _veilidLoggy.trace(log.message, error, stackTrace); + veilidLoggy.trace(log.message, error, stackTrace); break; } } diff --git a/packages/veilid_support/lib/veilid_support.dart b/packages/veilid_support/lib/veilid_support.dart index f9e9293..56db796 100644 --- a/packages/veilid_support/lib/veilid_support.dart +++ b/packages/veilid_support/lib/veilid_support.dart @@ -12,4 +12,4 @@ export 'src/json_tools.dart'; export 'src/memory_tools.dart'; export 'src/protobuf_tools.dart'; export 'src/table_db.dart'; -export 'src/veilid_log.dart'; +export 'src/veilid_log.dart' hide veilidLoggy; diff --git a/pubspec.lock b/pubspec.lock index 2a2bec9..c1c3af8 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1618,4 +1618,4 @@ packages: version: "1.1.2" sdks: dart: ">=3.3.0 <4.0.0" - flutter: ">=3.19.0" + flutter: ">=3.19.1" diff --git a/pubspec.yaml b/pubspec.yaml index 8b2341d..6fa782e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -5,7 +5,7 @@ version: 1.0.2+0 environment: sdk: '>=3.2.0 <4.0.0' - flutter: ">=3.10.0" + flutter: '>=3.19.1' dependencies: animated_theme_switcher: ^2.0.10 From cd329c7bad37fe712e6ddd589244e1e61bf2e5e9 Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Fri, 1 Mar 2024 20:28:02 -0500 Subject: [PATCH 58/68] fix messages select --- lib/chat/cubits/messages_cubit.dart | 4 +++- lib/chat/views/chat_component.dart | 25 ++++++++++++------------- 2 files changed, 15 insertions(+), 14 deletions(-) diff --git a/lib/chat/cubits/messages_cubit.dart b/lib/chat/cubits/messages_cubit.dart index ee259dd..1e04641 100644 --- a/lib/chat/cubits/messages_cubit.dart +++ b/lib/chat/cubits/messages_cubit.dart @@ -16,7 +16,9 @@ class _MessageQueueEntry { IList remoteMessages; } -class MessagesCubit extends Cubit>> { +typedef MessagesState = AsyncValue>; + +class MessagesCubit extends Cubit { MessagesCubit( {required ActiveAccountInfo activeAccountInfo, required TypedKey remoteIdentityPublicKey, diff --git a/lib/chat/views/chat_component.dart b/lib/chat/views/chat_component.dart index decf840..8d829bb 100644 --- a/lib/chat/views/chat_component.dart +++ b/lib/chat/views/chat_component.dart @@ -19,20 +19,20 @@ import '../chat.dart'; class ChatComponent extends StatelessWidget { const ChatComponent._( {required TypedKey localUserIdentityKey, - required TypedKey remoteConversationRecordKey, required MessagesCubit messagesCubit, + required MessagesState messagesState, required types.User localUser, required types.User remoteUser, super.key}) : _localUserIdentityKey = localUserIdentityKey, - _remoteConversationRecordKey = remoteConversationRecordKey, _messagesCubit = messagesCubit, + _messagesState = messagesState, _localUser = localUser, _remoteUser = remoteUser; final TypedKey _localUserIdentityKey; - final TypedKey _remoteConversationRecordKey; final MessagesCubit _messagesCubit; + final MessagesState _messagesState; final types.User _localUser; final types.User _remoteUser; @@ -78,21 +78,21 @@ class ChatComponent extends StatelessWidget { firstName: editedName); // Get the messages cubit - final messagesCubit = context - .select( - (x) => x.tryOperate(remoteConversationRecordKey, - closure: (cubit) => cubit)); + final messages = context.select( + (x) => x.tryOperate(remoteConversationRecordKey, + closure: (cubit) => (cubit, cubit.state))); // Get the messages to display // and ensure it is safe to operate() on the MessageCubit for this chat - if (messagesCubit == null) { + if (messages == null) { return waitingPage(); } return ChatComponent._( localUserIdentityKey: localUserIdentityKey, - remoteConversationRecordKey: remoteConversationRecordKey, - messagesCubit: messagesCubit, + messagesCubit: messages.$1, + messagesState: messages.$2, localUser: localUser, remoteUser: remoteUser, key: key); @@ -140,10 +140,9 @@ class ChatComponent extends StatelessWidget { final textTheme = Theme.of(context).textTheme; final chatTheme = makeChatTheme(scale, textTheme); - final avmessages = _messagesCubit.state; - final messages = avmessages.data?.value; + final messages = _messagesState.data?.value; if (messages == null) { - return avmessages.buildNotData(); + return _messagesState.buildNotData(); } // Convert protobuf messages to chat messages From c9e0831555d52f097fa43116f7a8e416e5f2d313 Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Fri, 1 Mar 2024 21:14:11 -0500 Subject: [PATCH 59/68] more fixes --- ios/Flutter/AppFrameworkInfo.plist | 2 +- ios/Podfile | 2 +- ios/Podfile.lock | 31 ++++++++----------- ios/Runner.xcodeproj/project.pbxproj | 2 +- .../xcshareddata/xcschemes/Runner.xcscheme | 2 +- 5 files changed, 17 insertions(+), 22 deletions(-) diff --git a/ios/Flutter/AppFrameworkInfo.plist b/ios/Flutter/AppFrameworkInfo.plist index 9625e10..7c56964 100644 --- a/ios/Flutter/AppFrameworkInfo.plist +++ b/ios/Flutter/AppFrameworkInfo.plist @@ -21,6 +21,6 @@ CFBundleVersion 1.0 MinimumOSVersion - 11.0 + 12.0 diff --git a/ios/Podfile b/ios/Podfile index bd3431c..2cbcaa2 100644 --- a/ios/Podfile +++ b/ios/Podfile @@ -44,4 +44,4 @@ post_install do |installer| File.open(xcconfig_path, "w") { |file| file << xcconfig_mod } end end -end \ No newline at end of file +end diff --git a/ios/Podfile.lock b/ios/Podfile.lock index c858771..636ef83 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -4,9 +4,6 @@ PODS: - Flutter (1.0.0) - flutter_native_splash (0.0.1): - Flutter - - FMDB (2.7.5): - - FMDB/standard (= 2.7.5) - - FMDB/standard (2.7.5) - GoogleDataTransport (9.2.5): - GoogleUtilities/Environment (~> 7.7) - nanopb (< 2.30910.0, >= 2.30908.0) @@ -55,7 +52,7 @@ PODS: - GTMSessionFetcher/Core (< 3.0, >= 1.1) - MLImage (= 1.0.0-beta4) - MLKitCommon (~> 9.0) - - mobile_scanner (3.5.5): + - mobile_scanner (3.5.6): - Flutter - GoogleMLKit/BarcodeScanning (~> 4.0.0) - nanopb (2.30909.0): @@ -78,7 +75,7 @@ PODS: - Flutter - sqflite (0.0.3): - Flutter - - FMDB (>= 2.7.5) + - FlutterMacOS - system_info_plus (0.0.1): - Flutter - url_launcher_ios (0.0.1): @@ -96,14 +93,13 @@ DEPENDENCIES: - share_plus (from `.symlinks/plugins/share_plus/ios`) - shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`) - smart_auth (from `.symlinks/plugins/smart_auth/ios`) - - sqflite (from `.symlinks/plugins/sqflite/ios`) + - sqflite (from `.symlinks/plugins/sqflite/darwin`) - 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: trunk: - - FMDB - GoogleDataTransport - GoogleMLKit - GoogleToolboxForMac @@ -137,7 +133,7 @@ EXTERNAL SOURCES: smart_auth: :path: ".symlinks/plugins/smart_auth/ios" sqflite: - :path: ".symlinks/plugins/sqflite/ios" + :path: ".symlinks/plugins/sqflite/darwin" system_info_plus: :path: ".symlinks/plugins/system_info_plus/ios" url_launcher_ios: @@ -146,10 +142,9 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/veilid/ios" SPEC CHECKSUMS: - camera_avfoundation: 3125e8cd1a4387f6f31c6c63abb8a55892a9eeeb - Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854 + camera_avfoundation: 759172d1a77ae7be0de08fc104cfb79738b8a59e + Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 flutter_native_splash: 52501b97d1c0a5f898d687f1646226c1f93c56ef - FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a GoogleDataTransport: 54dee9d48d14580407f8f5fbf2f496e92437a2f2 GoogleMLKit: 2bd0dc6253c4d4f227aad460f69215a504b2980e GoogleToolboxForMac: 8bef7c7c5cf7291c687cf5354f39f9db6399ad34 @@ -160,19 +155,19 @@ SPEC CHECKSUMS: MLKitBarcodeScanning: 04e264482c5f3810cb89ebc134ef6b61e67db505 MLKitCommon: c1b791c3e667091918d91bda4bba69a91011e390 MLKitVision: 8baa5f46ee3352614169b85250574fde38c36f49 - mobile_scanner: 202ab6f652e40a9add68b10de4c4fb2a745c4348 + mobile_scanner: 38dcd8a49d7d485f632b7de65e4900010187aef2 nanopb: b552cce312b6c8484180ef47159bc0f65a1f0431 pasteboard: 982969ebaa7c78af3e6cc7761e8f5e77565d9ce0 - path_provider_foundation: 29f094ae23ebbca9d3d0cec13889cd9060c0e943 + path_provider_foundation: 3784922295ac71e43754bd15e0653ccfd36a147c PromisesObjC: c50d2056b5253dadbd6c2bea79b0674bd5a52fa4 share_plus: c3fef564749587fc939ef86ffb283ceac0baf9f5 - shared_preferences_foundation: 5b919d13b803cadd15ed2dc053125c68730e5126 + shared_preferences_foundation: b4c3b4cddf1c21f02770737f147a3f5da9d39695 smart_auth: 4bedbc118723912d0e45a07e8ab34039c19e04f2 - sqflite: 31f7eba61e3074736dff8807a9b41581e4f7f15a + sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec system_info_plus: 5393c8da281d899950d751713575fbf91c7709aa - url_launcher_ios: bf5ce03e0e2088bad9cc378ea97fa0ed5b49673b + url_launcher_ios: 6116280ddcfe98ab8820085d8d76ae7449447586 veilid: f5c2e662f91907b30cf95762619526ac3e4512fd -PODFILE CHECKSUM: 7f4cf2154d55730d953b184299e6feee7a274740 +PODFILE CHECKSUM: 5d504085cd7c7a4d71ee600d7af087cb60ab75b2 -COCOAPODS: 1.12.1 +COCOAPODS: 1.15.2 diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 3100ff0..72c3178 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -155,7 +155,7 @@ 97C146E61CF9000F007C117D /* Project object */ = { isa = PBXProject; attributes = { - LastUpgradeCheck = 1430; + LastUpgradeCheck = 1510; ORGANIZATIONNAME = ""; TargetAttributes = { 97C146ED1CF9000F007C117D = { diff --git a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index a6b826d..5e31d3d 100644 --- a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -1,6 +1,6 @@ Date: Tue, 5 Mar 2024 15:29:02 -0500 Subject: [PATCH 60/68] simplify config and fix refresh --- .../account_repository.dart | 7 +-- .../lib/dht_support/src/dht_record_pool.dart | 4 +- .../lib/dht_support/src/dht_short_array.dart | 2 +- packages/veilid_support/lib/src/config.dart | 44 +++++++------------ 4 files changed, 23 insertions(+), 34 deletions(-) diff --git a/lib/account_manager/repository/account_repository/account_repository.dart b/lib/account_manager/repository/account_repository/account_repository.dart index 1b145b3..39b1261 100644 --- a/lib/account_manager/repository/account_repository/account_repository.dart +++ b/lib/account_manager/repository/account_repository/account_repository.dart @@ -343,13 +343,14 @@ class AccountRepository { await _userLogins.set(newUserLogins); await _activeLocalAccount.set(identityMaster.masterRecordKey); - _streamController - ..add(AccountRepositoryChange.userLogins) - ..add(AccountRepositoryChange.activeLocalAccount); // Ensure all logins are opened await _openLoggedInDHTRecords(); + _streamController + ..add(AccountRepositoryChange.userLogins) + ..add(AccountRepositoryChange.activeLocalAccount); + return true; } diff --git a/packages/veilid_support/lib/dht_support/src/dht_record_pool.dart b/packages/veilid_support/lib/dht_support/src/dht_record_pool.dart index 7389123..80d1d43 100644 --- a/packages/veilid_support/lib/dht_support/src/dht_record_pool.dart +++ b/packages/veilid_support/lib/dht_support/src/dht_record_pool.dart @@ -569,7 +569,7 @@ class DHTRecordPool with TableDBBacked { subkeys: allSubkeys, expiration: maxExpiration, count: totalCount); } - void _updateWatchExpirations( + void _updateWatchRealExpirations( Iterable records, Timestamp realExpiration) { for (final rec in records) { final ws = rec.watchState; @@ -636,7 +636,7 @@ class DHTRecordPool with TableDBBacked { // Update watch states with real expiration if (realExpiration.value != BigInt.zero) { openedRecordInfo.shared.needsWatchStateUpdate = false; - _updateWatchExpirations( + _updateWatchRealExpirations( openedRecordInfo.records, realExpiration); success = true; } diff --git a/packages/veilid_support/lib/dht_support/src/dht_short_array.dart b/packages/veilid_support/lib/dht_support/src/dht_short_array.dart index 8cc0e37..3de5df5 100644 --- a/packages/veilid_support/lib/dht_support/src/dht_short_array.dart +++ b/packages/veilid_support/lib/dht_support/src/dht_short_array.dart @@ -759,7 +759,7 @@ class DHTShortArray { for (final skr in subkeys) { for (var subkey = skr.low; subkey <= skr.high; subkey++) { // Skip head subkey - if (subkey == 0) { + if (updateHead && subkey == 0) { continue; } // Get the subkey, which caches the result in the local record store diff --git a/packages/veilid_support/lib/src/config.dart b/packages/veilid_support/lib/src/config.dart index dff9f3e..c13a8b8 100644 --- a/packages/veilid_support/lib/src/config.dart +++ b/packages/veilid_support/lib/src/config.dart @@ -1,4 +1,5 @@ import 'package:veilid/veilid.dart'; +import 'dart:io' show Platform; Map getDefaultVeilidPlatformConfig( bool isWeb, String appName) { @@ -43,8 +44,18 @@ Map getDefaultVeilidPlatformConfig( .toJson(); } -Future getVeilidConfig(bool isWeb, String appName) async { - var config = await getDefaultVeilidConfig(appName); +Future getVeilidConfig(bool isWeb, String programName) async { + var config = await getDefaultVeilidConfig( + isWeb: isWeb, + programName: programName, + // ignore: avoid_redundant_argument_values, do_not_use_environment + namespace: const String.fromEnvironment('NAMESPACE'), + // ignore: avoid_redundant_argument_values, do_not_use_environment + bootstrap: const String.fromEnvironment('BOOTSTRAP'), + // ignore: avoid_redundant_argument_values, do_not_use_environment + networkKeyPassword: const String.fromEnvironment('NETWORK_KEY'), + ); + // ignore: do_not_use_environment if (const String.fromEnvironment('DELETE_TABLE_STORE') == '1') { config = @@ -73,35 +84,12 @@ Future getVeilidConfig(bool isWeb, String appName) async { config.network.routingTable.copyWith(bootstrap: bootstrap))); } - // ignore: do_not_use_environment - const envNetworkKey = String.fromEnvironment('NETWORK_KEY'); - if (envNetworkKey.isNotEmpty) { - config = config.copyWith( - network: config.network.copyWith(networkKeyPassword: envNetworkKey)); - } - - // ignore: do_not_use_environment - const envBootstrap = String.fromEnvironment('BOOTSTRAP'); - if (envBootstrap.isNotEmpty) { - final bootstrap = envBootstrap.split(',').map((e) => e.trim()).toList(); - config = config.copyWith( - network: config.network.copyWith( - routingTable: - config.network.routingTable.copyWith(bootstrap: bootstrap))); - } - return config.copyWith( capabilities: // XXX: Remove DHTV and DHTW when we get background sync implemented const VeilidConfigCapabilities(disable: ['DHTV', 'DHTW', 'TUNL']), - protectedStore: config.protectedStore.copyWith(allowInsecureFallback: true), - // network: config.network.copyWith( - // dht: config.network.dht.copyWith( - // getValueCount: 3, - // getValueFanout: 8, - // getValueTimeoutMs: 5000, - // setValueCount: 4, - // setValueFanout: 10, - // setValueTimeoutMs: 5000)) + protectedStore: + // XXX: Linux often does not have a secret storage mechanism installed + config.protectedStore.copyWith(allowInsecureFallback: Platform.isLinux), ); } From 64d4d0cefb6e86fd04dcc4d67ac8dd3f52c13caf Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Thu, 7 Mar 2024 08:28:05 -0500 Subject: [PATCH 61/68] short array work and doc --- lib/chat/cubits/messages_cubit.dart | 144 +++++++++--------- lib/settings/preferences_repository.dart | 4 +- lib/tools/state_logger.dart | 3 +- .../repository/processor_repository.dart | 2 + .../lib/dht_support/src/dht_short_array.dart | 103 +++++++++++-- .../src/dht_short_array_cubit.dart | 16 +- 6 files changed, 178 insertions(+), 94 deletions(-) diff --git a/lib/chat/cubits/messages_cubit.dart b/lib/chat/cubits/messages_cubit.dart index 1e04641..ee09893 100644 --- a/lib/chat/cubits/messages_cubit.dart +++ b/lib/chat/cubits/messages_cubit.dart @@ -10,9 +10,7 @@ import '../../account_manager/account_manager.dart'; import '../../proto/proto.dart' as proto; class _MessageQueueEntry { - _MessageQueueEntry( - {required this.localMessages, required this.remoteMessages}); - IList localMessages; + _MessageQueueEntry({required this.remoteMessages}); IList remoteMessages; } @@ -60,74 +58,10 @@ class MessagesCubit extends Cubit { await super.close(); } - void updateLocalMessagesState( - BlocBusyState>> avmessages) { - // Updated local messages from online just update the state immediately - emit(avmessages.state); - } - - Future _updateRemoteMessagesStateAsync(_MessageQueueEntry entry) async { - // Updated remote messages need to be merged with the local messages state - - // Ensure remoteMessages is sorted by timestamp - final remoteMessages = - entry.remoteMessages.sort((a, b) => a.timestamp.compareTo(b.timestamp)); - - // Existing messages will always be sorted by timestamp so merging is easy - var localMessages = entry.localMessages; - var pos = 0; - for (final newMessage in remoteMessages) { - var skip = false; - while (pos < localMessages.length) { - final m = localMessages[pos]; - - // If timestamp to insert is less than - // the current position, insert it here - final newTs = Timestamp.fromInt64(newMessage.timestamp); - final curTs = Timestamp.fromInt64(m.timestamp); - final cmp = newTs.compareTo(curTs); - if (cmp < 0) { - break; - } else if (cmp == 0) { - skip = true; - break; - } - pos++; - } - // Insert at this position - if (!skip) { - // Insert into dht backing array - await _localMessagesCubit!.operate((shortArray) => - shortArray.tryInsertItem(pos, newMessage.writeToBuffer())); - // Insert into local copy as well for this operation - localMessages = localMessages.insert(pos, newMessage); - } - } - } - - void updateRemoteMessagesState( - BlocBusyState>> avmessages) { - final remoteMessages = avmessages.state.data?.value; - if (remoteMessages == null) { - return; - } - - final localMessages = state.data?.value; - if (localMessages == null) { - // No local messages means remote messages - // are all we have so merging is easy - emit(AsyncValue.data(remoteMessages)); - return; - } - - _remoteMessagesQueue.add(_MessageQueueEntry( - localMessages: localMessages, remoteMessages: remoteMessages)); - } - // Open local messages key Future _initLocalMessages(TypedKey localConversationRecordKey, TypedKey localMessagesRecordKey) async { - final crypto = await getMessagesCrypto(); + final crypto = await _getMessagesCrypto(); final writer = _activeAccountInfo.conversationWriter; _localMessagesCubit = DHTShortArrayCubit( @@ -136,21 +70,87 @@ class MessagesCubit extends Cubit { parent: localConversationRecordKey, crypto: crypto), decodeElement: proto.Message.fromBuffer); _localSubscription = - _localMessagesCubit!.stream.listen(updateLocalMessagesState); + _localMessagesCubit!.stream.listen(_updateLocalMessagesState); + _updateLocalMessagesState(_localMessagesCubit!.state); } // Open remote messages key Future _initRemoteMessages(TypedKey remoteConversationRecordKey, TypedKey remoteMessagesRecordKey) async { // Open remote record key if it is specified - final crypto = await getMessagesCrypto(); + final crypto = await _getMessagesCrypto(); _remoteMessagesCubit = DHTShortArrayCubit( open: () async => DHTShortArray.openRead(remoteMessagesRecordKey, parent: remoteConversationRecordKey, crypto: crypto), decodeElement: proto.Message.fromBuffer); _remoteSubscription = - _remoteMessagesCubit!.stream.listen(updateRemoteMessagesState); + _remoteMessagesCubit!.stream.listen(_updateRemoteMessagesState); + _updateRemoteMessagesState(_remoteMessagesCubit!.state); + } + + // Called when the local messages list gets a change + void _updateLocalMessagesState( + BlocBusyState>> avmessages) { + // When local messages are updated, pass this + // directly to the messages cubit state + emit(avmessages.state); + } + + // Called when the remote messages list gets a change + void _updateRemoteMessagesState( + BlocBusyState>> avmessages) { + final remoteMessages = avmessages.state.data?.value; + if (remoteMessages == null) { + return; + } + // Add remote messages updates to queue to process asynchronously + _remoteMessagesQueue + .add(_MessageQueueEntry(remoteMessages: remoteMessages)); + } + + Future _updateRemoteMessagesStateAsync(_MessageQueueEntry entry) async { + final localMessagesCubit = _localMessagesCubit!; + + // Updated remote messages need to be merged with the local messages state + await localMessagesCubit.operate((shortArray) async { + // Ensure remoteMessages is sorted by timestamp + final remoteMessages = entry.remoteMessages + .sort((a, b) => a.timestamp.compareTo(b.timestamp)); + + // dedup? build local timestamp set? + + // Existing messages will always be sorted by timestamp so merging is easy + var localMessages = localMessagesCubit.state.state.data!.value; + + var pos = 0; + for (final newMessage in remoteMessages) { + var skip = false; + while (pos < localMessages.length) { + final m = localMessages[pos]; + pos++; + + // If timestamp to insert is less than + // the current position, insert it here + final newTs = Timestamp.fromInt64(newMessage.timestamp); + final curTs = Timestamp.fromInt64(m.timestamp); + final cmp = newTs.compareTo(curTs); + if (cmp < 0) { + break; + } else if (cmp == 0) { + skip = true; + break; + } + } + // Insert at this position + if (!skip) { + // Insert into dht backing array + await shortArray.tryInsertItem(pos, newMessage.writeToBuffer()); + // Insert into local copy as well for this operation + localMessages = localMessages.insert(pos, newMessage); + } + } + }); } // Initialize local messages @@ -187,7 +187,7 @@ class MessagesCubit extends Cubit { (shortArray) => shortArray.tryAddItem(message.writeToBuffer())); } - Future getMessagesCrypto() async { + Future _getMessagesCrypto() async { var messagesCrypto = _messagesCrypto; if (messagesCrypto != null) { return messagesCrypto; diff --git a/lib/settings/preferences_repository.dart b/lib/settings/preferences_repository.dart index d7b8f48..03f73ba 100644 --- a/lib/settings/preferences_repository.dart +++ b/lib/settings/preferences_repository.dart @@ -20,9 +20,11 @@ class PreferencesRepository { Future init() async { final sharedPreferences = await SharedPreferences.getInstance(); + // ignore: do_not_use_environment + const namespace = String.fromEnvironment('NAMESPACE'); _data = SharedPreferencesValue( sharedPreferences: sharedPreferences, - keyName: 'preferences', + keyName: namespace.isEmpty ? 'preferences' : 'preferences_$namespace', valueFromJson: (obj) => obj != null ? Preferences.fromJson(obj) : Preferences.defaults, valueToJson: (val) => val.toJson()); diff --git a/lib/tools/state_logger.dart b/lib/tools/state_logger.dart index 0f27504..60c274e 100644 --- a/lib/tools/state_logger.dart +++ b/lib/tools/state_logger.dart @@ -3,7 +3,8 @@ import 'package:loggy/loggy.dart'; import 'loggy.dart'; const Map _blocChangeLogLevels = { - 'ConnectionStateCubit': LogLevel.off + 'ConnectionStateCubit': LogLevel.off, + 'ActiveConversationMessagesBlocMapCubit': LogLevel.off }; const Map _blocCreateCloseLogLevels = {}; const Map _blocErrorLogLevels = {}; diff --git a/lib/veilid_processor/repository/processor_repository.dart b/lib/veilid_processor/repository/processor_repository.dart index 0a17fc7..e021648 100644 --- a/lib/veilid_processor/repository/processor_repository.dart +++ b/lib/veilid_processor/repository/processor_repository.dart @@ -119,6 +119,8 @@ class ProcessorRepository { } void processUpdateValueChange(VeilidUpdateValueChange updateValueChange) { + log.debug('UpdateValueChange: ${updateValueChange.toJson()}'); + // Send value updates to DHTRecordPool DHTRecordPool.instance.processRemoteValueChange(updateValueChange); } diff --git a/packages/veilid_support/lib/dht_support/src/dht_short_array.dart b/packages/veilid_support/lib/dht_support/src/dht_short_array.dart index 3de5df5..9205fce 100644 --- a/packages/veilid_support/lib/dht_support/src/dht_short_array.dart +++ b/packages/veilid_support/lib/dht_support/src/dht_short_array.dart @@ -36,11 +36,7 @@ class DHTShortArray { //////////////////////////////////////////////////////////////// // Constructors - DHTShortArray._({required DHTRecord headRecord}) - : _headRecord = headRecord, - _head = _DHTShortArrayCache(), - _subscriptions = {}, - _listenMutex = Mutex() { + DHTShortArray._({required DHTRecord headRecord}) : _headRecord = headRecord { late final int stride; switch (headRecord.schema) { case DHTSchemaDFLT(oCnt: final oCnt): @@ -155,9 +151,14 @@ class DHTShortArray { //////////////////////////////////////////////////////////////////////////// // Public API + /// Returns the first record in the DHTShortArray which contains its control + /// information. DHTRecord get record => _headRecord; + + /// Returns the number of elements in the DHTShortArray int get length => _head.index.length; + /// Free all resources for the DHTShortArray Future close() async { await _watchController?.close(); final futures = >[_headRecord.close()]; @@ -167,6 +168,7 @@ class DHTShortArray { await Future.wait(futures); } + /// Free all resources for the DHTShortArray and delete it from the DHT Future delete() async { await _watchController?.close(); @@ -177,6 +179,8 @@ class DHTShortArray { await Future.wait(futures); } + /// Runs a closure that guarantees the DHTShortArray + /// will be closed upon exit, even if an uncaught exception is thrown Future scope(Future Function(DHTShortArray) scopeFunction) async { try { return await scopeFunction(this); @@ -185,6 +189,9 @@ class DHTShortArray { } } + /// Runs a closure that guarantees the DHTShortArray + /// will be closed upon exit, and deleted if an an + /// uncaught exception is thrown Future deleteScope( Future Function(DHTShortArray) scopeFunction) async { try { @@ -197,6 +204,9 @@ class DHTShortArray { } } + /// Return the item at position 'pos' in the DHTShortArray. If 'foreRefresh' + /// is specified, the network will always be checked for newer values + /// rather than returning the existing locally stored copy of the elements. Future getItem(int pos, {bool forceRefresh = false}) async { await _refreshHead(forceRefresh: forceRefresh, onlyUpdates: true); @@ -212,6 +222,9 @@ class DHTShortArray { return record!.get(subkey: recordSubkey, forceRefresh: forceRefresh); } + /// Return a list of all of the items in the DHTShortArray. If 'foreRefresh' + /// is specified, the network will always be checked for newer values + /// rather than returning the existing locally stored copy of the elements. Future?> getAllItems({bool forceRefresh = false}) async { await _refreshHead(forceRefresh: forceRefresh, onlyUpdates: true); @@ -238,28 +251,41 @@ class DHTShortArray { return out; } + /// Convenience function: + /// Like getItem but also parses the returned element as JSON Future getItemJson(T Function(dynamic) fromJson, int pos, {bool forceRefresh = false}) => getItem(pos, forceRefresh: forceRefresh) .then((out) => jsonDecodeOptBytes(fromJson, out)); + /// Convenience function: + /// Like getAllItems but also parses the returned elements as JSON Future?> getAllItemsJson(T Function(dynamic) fromJson, {bool forceRefresh = false}) => getAllItems(forceRefresh: forceRefresh) .then((out) => out?.map(fromJson).toList()); + /// Convenience function: + /// Like getItem but also parses the returned element as a protobuf object Future getItemProtobuf( T Function(List) fromBuffer, int pos, {bool forceRefresh = false}) => getItem(pos, forceRefresh: forceRefresh) .then((out) => (out == null) ? null : fromBuffer(out)); + /// Convenience function: + /// Like getAllItems but also parses the returned elements as protobuf objects Future?> getAllItemsProtobuf( T Function(List) fromBuffer, {bool forceRefresh = false}) => getAllItems(forceRefresh: forceRefresh) .then((out) => out?.map(fromBuffer).toList()); + /// Try to add an item to the end of the DHTShortArray. Return true if the + /// element was successfully added, and false if the state changed before + /// the element could be added or a newer value was found on the network. + /// This may throw an exception if the number elements added exceeds the + /// built-in limit of 'maxElements = 256' entries. Future tryAddItem(Uint8List value) async { await _refreshHead(onlyUpdates: true); @@ -288,6 +314,12 @@ class DHTShortArray { return true; } + /// Try to insert an item as position 'pos' of the DHTShortArray. + /// Return true if the element was successfully inserted, and false if the + /// state changed before the element could be inserted or a newer value was + /// found on the network. + /// This may throw an exception if the number elements added exceeds the + /// built-in limit of 'maxElements = 256' entries. Future tryInsertItem(int pos, Uint8List value) async { await _refreshHead(onlyUpdates: true); @@ -314,6 +346,10 @@ class DHTShortArray { return true; } + /// Try to swap items at position 'aPos' and 'bPos' in the DHTShortArray. + /// Return true if the elements were successfully swapped, and false if the + /// state changed before the elements could be swapped or newer values were + /// found on the network. Future trySwapItem(int aPos, int bPos) async { if (aPos == bPos) { return false; @@ -344,6 +380,10 @@ class DHTShortArray { return true; } + /// Try to remove an item at position 'pos' in the DHTShortArray. + /// Return the element if it was successfully removed, and null if the + /// state changed before the elements could be removed or newer values were + /// found on the network. Future tryRemoveItem(int pos) async { await _refreshHead(onlyUpdates: true); @@ -376,16 +416,24 @@ class DHTShortArray { } } + /// Convenience function: + /// Like removeItem but also parses the returned element as JSON Future tryRemoveItemJson( T Function(dynamic) fromJson, int pos, ) => tryRemoveItem(pos).then((out) => jsonDecodeOptBytes(fromJson, out)); + /// Convenience function: + /// Like removeItem but also parses the returned element as JSON Future tryRemoveItemProtobuf( T Function(List) fromBuffer, int pos) => getItem(pos).then((out) => (out == null) ? null : fromBuffer(out)); + /// Try to remove all items in the DHTShortArray. + /// Return true if it was successfully cleared, and false if the + /// state changed before the elements could be cleared or newer values were + /// found on the network. Future tryClear() async { await _refreshHead(onlyUpdates: true); @@ -411,6 +459,12 @@ class DHTShortArray { return true; } + /// Try to set an item at position 'pos' of the DHTShortArray. + /// Return true if the element was successfully set, and false if the + /// state changed before the element could be set or a newer value was + /// found on the network. + /// This may throw an exception if the position elements the built-in limit of + /// 'maxElements = 256' entries. Future tryWriteItem(int pos, Uint8List newValue) async { if (await _refreshHead(onlyUpdates: true)) { throw StateError('structure changed'); @@ -433,6 +487,10 @@ class DHTShortArray { return result; } + /// Set an item at position 'pos' of the DHTShortArray. Retries until the + /// value being written is successfully made the newest value of the element. + /// This may throw an exception if the position elements the built-in limit of + /// 'maxElements = 256' entries. Future eventualWriteItem(int pos, Uint8List newValue) async { Uint8List? oldData; do { @@ -443,6 +501,12 @@ class DHTShortArray { } while (oldData != null); } + /// Change an item at position 'pos' of the DHTShortArray. + /// Runs with the value of the old element at that position such that it can + /// be changed to the returned value from tha closure. Retries until the + /// value being written is successfully made the newest value of the element. + /// This may throw an exception if the position elements the built-in limit of + /// 'maxElements = 256' entries. Future eventualUpdateItem( int pos, Future Function(Uint8List? oldValue) update) async { var oldData = await getItem(pos); @@ -461,6 +525,9 @@ class DHTShortArray { } while (oldData != null); } + /// Convenience function: + /// Like tryWriteItem but also encodes the input value as JSON and parses the + /// returned element as JSON Future tryWriteItemJson( T Function(dynamic) fromJson, int pos, @@ -469,6 +536,9 @@ class DHTShortArray { tryWriteItem(pos, jsonEncodeBytes(newValue)) .then((out) => jsonDecodeOptBytes(fromJson, out)); + /// Convenience function: + /// Like tryWriteItem but also encodes the input value as a protobuf object + /// and parses the returned element as a protobuf object Future tryWriteItemProtobuf( T Function(List) fromBuffer, int pos, @@ -481,14 +551,22 @@ class DHTShortArray { return fromBuffer(out); }); + /// Convenience function: + /// Like eventualWriteItem but also encodes the input value as JSON and parses + /// the returned element as JSON Future eventualWriteItemJson(int pos, T newValue) => eventualWriteItem(pos, jsonEncodeBytes(newValue)); + /// Convenience function: + /// Like eventualWriteItem but also encodes the input value as a protobuf + /// object and parses the returned element as a protobuf object Future eventualWriteItemProtobuf( int pos, T newValue, {int subkey = -1}) => eventualWriteItem(pos, newValue.writeToBuffer()); + /// Convenience function: + /// Like eventualUpdateItem but also encodes the input value as JSON Future eventualUpdateItemJson( T Function(dynamic) fromJson, int pos, @@ -496,6 +574,9 @@ class DHTShortArray { ) => eventualUpdateItem(pos, jsonUpdate(fromJson, update)); + /// Convenience function: + /// Like eventualUpdateItem but also encodes the input value as a protobuf + /// object Future eventualUpdateItemProtobuf( T Function(List) fromBuffer, int pos, @@ -807,15 +888,17 @@ class DHTShortArray { // Head DHT record final DHTRecord _headRecord; + // How many elements per linked record late final int _stride; - // Cached representation refreshed from head record - _DHTShortArrayCache _head; - + _DHTShortArrayCache _head = _DHTShortArrayCache(); // Subscription to head and linked record internal changes - final Map> _subscriptions; + final Map> _subscriptions = + {}; // Stream of external changes StreamController? _watchController; // Watch mutex to ensure we keep the representation valid - final Mutex _listenMutex; + final Mutex _listenMutex = Mutex(); + // Head/element mutex to ensure we keep the representation valid + final Mutex _headMutex = Mutex(); } diff --git a/packages/veilid_support/lib/dht_support/src/dht_short_array_cubit.dart b/packages/veilid_support/lib/dht_support/src/dht_short_array_cubit.dart index 7937b12..8525721 100644 --- a/packages/veilid_support/lib/dht_support/src/dht_short_array_cubit.dart +++ b/packages/veilid_support/lib/dht_support/src/dht_short_array_cubit.dart @@ -48,9 +48,8 @@ class DHTShortArrayCubit extends Cubit> } Future _refreshNoWait({bool forceRefresh = false}) async => - busy((emit) async { - await _refreshInner(emit, forceRefresh: forceRefresh); - }); + busy((emit) async => _operateMutex.protect( + () async => _refreshInner(emit, forceRefresh: forceRefresh))); Future _refreshInner(void Function(AsyncValue>) emit, {bool forceRefresh = false}) async { @@ -59,9 +58,7 @@ class DHTShortArrayCubit extends Cubit> (await _shortArray.getAllItems(forceRefresh: forceRefresh)) ?.map(_decodeElement) .toIList(); - if (newState == null) { - emit(const AsyncValue.loading()); - } else { + if (newState != null) { emit(AsyncValue.data(newState)); } } on Exception catch (e) { @@ -74,9 +71,8 @@ class DHTShortArrayCubit extends Cubit> // Because this is async, we could get an update while we're // still processing the last one. Only called after init future has run // so we dont have to wait for that here. - _sspUpdate.busyUpdate>>(busy, (emit) async { - await _refreshInner(emit); - }); + _sspUpdate.busyUpdate>>(busy, + (emit) async => _operateMutex.protect(() async => _refreshInner(emit))); } @override @@ -90,7 +86,7 @@ class DHTShortArrayCubit extends Cubit> await super.close(); } - Future operate(Future Function(DHTShortArray) closure) async { + Future operate(Future Function(DHTShortArray) closure) async { await _initFuture; return _operateMutex.protect(() async => closure(_shortArray)); } From 41bb198d92cd05a3083110aabb869a7f0b1e2f9a Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Sun, 24 Mar 2024 12:13:27 -0400 Subject: [PATCH 62/68] xfer --- .../models/active_account_info.dart | 16 + .../account_repository.dart | 6 +- lib/chat/cubits/cubits.dart | 2 +- lib/chat/cubits/messages_cubit.dart | 225 ----- .../cubits/single_contact_messages_cubit.dart | 273 ++++++ lib/chat/views/chat_component.dart | 12 +- ..._conversation_messages_bloc_map_cubit.dart | 68 -- .../active_conversations_bloc_map_cubit.dart | 7 +- ...ve_single_contact_chat_bloc_map_cubit.dart | 108 +++ lib/chat_list/cubits/chat_list_cubit.dart | 35 +- lib/chat_list/cubits/cubits.dart | 2 +- .../chat_single_contact_list_widget.dart | 4 +- lib/contact_invitation/cubits/cubits.dart | 1 + .../cubits/invitation_generator_cubit.dart | 8 + .../views/contact_invitation_display.dart | 6 +- .../views/contact_invitation_item_widget.dart | 6 +- lib/contacts/cubits/conversation_cubit.dart | 37 +- .../home_account_ready_shell.dart | 11 +- lib/proto/veilidchat.pb.dart | 24 +- lib/proto/veilidchat.pbjson.dart | 10 +- lib/proto/veilidchat.proto | 17 +- packages/bloc_tools/lib/src/future_cubit.dart | 1 + .../lib/dht_support/dht_support.dart | 7 +- .../lib/dht_support/proto/dht.proto | 4 + .../dht_support/src/dht_record/barrel.dart | 3 + .../src/{ => dht_record}/dht_record.dart | 5 + .../{ => dht_record}/dht_record_crypto.dart | 2 +- .../{ => dht_record}/dht_record_cubit.dart | 2 +- .../src/{ => dht_record}/dht_record_pool.dart | 1 - .../dht_record_pool.freezed.dart | 2 +- .../{ => dht_record}/dht_record_pool.g.dart | 0 .../lib/dht_support/src/dht_short_array.dart | 904 ------------------ .../src/dht_short_array/barrel.dart | 2 + .../src/dht_short_array/dht_short_array.dart | 596 ++++++++++++ .../dht_short_array_cubit.dart | 4 +- .../dht_short_array/dht_short_array_head.dart | 471 +++++++++ packages/veilid_support/lib/proto/dht.pb.dart | 4 + .../veilid_support/lib/proto/dht.pbjson.dart | 3 +- .../lib/src/identity.freezed.dart | 2 +- .../veilid_support/lib/src/veilid_log.dart | 4 + 40 files changed, 1623 insertions(+), 1272 deletions(-) delete mode 100644 lib/chat/cubits/messages_cubit.dart create mode 100644 lib/chat/cubits/single_contact_messages_cubit.dart delete mode 100644 lib/chat_list/cubits/active_conversation_messages_bloc_map_cubit.dart create mode 100644 lib/chat_list/cubits/active_single_contact_chat_bloc_map_cubit.dart create mode 100644 lib/contact_invitation/cubits/invitation_generator_cubit.dart create mode 100644 packages/veilid_support/lib/dht_support/src/dht_record/barrel.dart rename packages/veilid_support/lib/dht_support/src/{ => dht_record}/dht_record.dart (98%) rename packages/veilid_support/lib/dht_support/src/{ => dht_record}/dht_record_crypto.dart (97%) rename packages/veilid_support/lib/dht_support/src/{ => dht_record}/dht_record_cubit.dart (99%) rename packages/veilid_support/lib/dht_support/src/{ => dht_record}/dht_record_pool.dart (99%) rename packages/veilid_support/lib/dht_support/src/{ => dht_record}/dht_record_pool.freezed.dart (99%) rename packages/veilid_support/lib/dht_support/src/{ => dht_record}/dht_record_pool.g.dart (100%) delete mode 100644 packages/veilid_support/lib/dht_support/src/dht_short_array.dart create mode 100644 packages/veilid_support/lib/dht_support/src/dht_short_array/barrel.dart create mode 100644 packages/veilid_support/lib/dht_support/src/dht_short_array/dht_short_array.dart rename packages/veilid_support/lib/dht_support/src/{ => dht_short_array}/dht_short_array_cubit.dart (97%) create mode 100644 packages/veilid_support/lib/dht_support/src/dht_short_array/dht_short_array_head.dart diff --git a/lib/account_manager/models/active_account_info.dart b/lib/account_manager/models/active_account_info.dart index 6a6afe0..5b556d5 100644 --- a/lib/account_manager/models/active_account_info.dart +++ b/lib/account_manager/models/active_account_info.dart @@ -1,3 +1,5 @@ +import 'dart:convert'; + import 'package:meta/meta.dart'; import 'package:veilid_support/veilid_support.dart'; @@ -22,6 +24,20 @@ class ActiveAccountInfo { return KeyPair(key: identityKey, secret: identitySecret.value); } + Future makeConversationCrypto( + TypedKey remoteIdentityPublicKey) async { + final identitySecret = userLogin.identitySecret; + final cs = await Veilid.instance.getCryptoSystem(identitySecret.kind); + final sharedSecret = await cs.generateSharedSecret( + remoteIdentityPublicKey.value, + identitySecret.value, + utf8.encode('VeilidChat Conversation')); + + final messagesCrypto = await DHTRecordCryptoPrivate.fromSecret( + identitySecret.kind, sharedSecret); + return messagesCrypto; + } + // final LocalAccount localAccount; final UserLogin userLogin; diff --git a/lib/account_manager/repository/account_repository/account_repository.dart b/lib/account_manager/repository/account_repository/account_repository.dart index 39b1261..bbc1351 100644 --- a/lib/account_manager/repository/account_repository/account_repository.dart +++ b/lib/account_manager/repository/account_repository/account_repository.dart @@ -218,18 +218,18 @@ class AccountRepository { // Make empty contact list log.debug('Creating contacts list'); final contactList = await (await DHTShortArray.create(parent: parent)) - .scope((r) async => r.record.ownedDHTRecordPointer); + .scope((r) async => r.recordPointer); // Make empty contact invitation record list log.debug('Creating contact invitation records list'); final contactInvitationRecords = await (await DHTShortArray.create(parent: parent)) - .scope((r) async => r.record.ownedDHTRecordPointer); + .scope((r) async => r.recordPointer); // Make empty chat record list log.debug('Creating chat records list'); final chatRecords = await (await DHTShortArray.create(parent: parent)) - .scope((r) async => r.record.ownedDHTRecordPointer); + .scope((r) async => r.recordPointer); // Make account object final account = proto.Account() diff --git a/lib/chat/cubits/cubits.dart b/lib/chat/cubits/cubits.dart index cb7ba44..b80767f 100644 --- a/lib/chat/cubits/cubits.dart +++ b/lib/chat/cubits/cubits.dart @@ -1,2 +1,2 @@ export 'active_chat_cubit.dart'; -export 'messages_cubit.dart'; +export 'single_contact_messages_cubit.dart'; diff --git a/lib/chat/cubits/messages_cubit.dart b/lib/chat/cubits/messages_cubit.dart deleted file mode 100644 index ee09893..0000000 --- a/lib/chat/cubits/messages_cubit.dart +++ /dev/null @@ -1,225 +0,0 @@ -import 'dart:async'; - -import 'package:async_tools/async_tools.dart'; -import 'package:bloc_tools/bloc_tools.dart'; -import 'package:fast_immutable_collections/fast_immutable_collections.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:veilid_support/veilid_support.dart'; - -import '../../account_manager/account_manager.dart'; -import '../../proto/proto.dart' as proto; - -class _MessageQueueEntry { - _MessageQueueEntry({required this.remoteMessages}); - IList remoteMessages; -} - -typedef MessagesState = AsyncValue>; - -class MessagesCubit extends Cubit { - MessagesCubit( - {required ActiveAccountInfo activeAccountInfo, - required TypedKey remoteIdentityPublicKey, - required TypedKey localConversationRecordKey, - required TypedKey localMessagesRecordKey, - required TypedKey remoteConversationRecordKey, - required TypedKey remoteMessagesRecordKey}) - : _activeAccountInfo = activeAccountInfo, - _remoteIdentityPublicKey = remoteIdentityPublicKey, - _remoteMessagesQueue = StreamController(), - super(const AsyncValue.loading()) { - // Local messages key - Future.delayed( - Duration.zero, - () async => _initLocalMessages( - localConversationRecordKey, localMessagesRecordKey)); - - // Remote messages key - Future.delayed( - Duration.zero, - () async => _initRemoteMessages( - remoteConversationRecordKey, remoteMessagesRecordKey)); - - // Remote messages listener - Future.delayed(Duration.zero, () async { - await for (final entry in _remoteMessagesQueue.stream) { - await _updateRemoteMessagesStateAsync(entry); - } - }); - } - - @override - Future close() async { - await _remoteMessagesQueue.close(); - await _localSubscription?.cancel(); - await _remoteSubscription?.cancel(); - await _localMessagesCubit?.close(); - await _remoteMessagesCubit?.close(); - await super.close(); - } - - // Open local messages key - Future _initLocalMessages(TypedKey localConversationRecordKey, - TypedKey localMessagesRecordKey) async { - final crypto = await _getMessagesCrypto(); - final writer = _activeAccountInfo.conversationWriter; - - _localMessagesCubit = DHTShortArrayCubit( - open: () async => DHTShortArray.openWrite( - localMessagesRecordKey, writer, - parent: localConversationRecordKey, crypto: crypto), - decodeElement: proto.Message.fromBuffer); - _localSubscription = - _localMessagesCubit!.stream.listen(_updateLocalMessagesState); - _updateLocalMessagesState(_localMessagesCubit!.state); - } - - // Open remote messages key - Future _initRemoteMessages(TypedKey remoteConversationRecordKey, - TypedKey remoteMessagesRecordKey) async { - // Open remote record key if it is specified - final crypto = await _getMessagesCrypto(); - - _remoteMessagesCubit = DHTShortArrayCubit( - open: () async => DHTShortArray.openRead(remoteMessagesRecordKey, - parent: remoteConversationRecordKey, crypto: crypto), - decodeElement: proto.Message.fromBuffer); - _remoteSubscription = - _remoteMessagesCubit!.stream.listen(_updateRemoteMessagesState); - _updateRemoteMessagesState(_remoteMessagesCubit!.state); - } - - // Called when the local messages list gets a change - void _updateLocalMessagesState( - BlocBusyState>> avmessages) { - // When local messages are updated, pass this - // directly to the messages cubit state - emit(avmessages.state); - } - - // Called when the remote messages list gets a change - void _updateRemoteMessagesState( - BlocBusyState>> avmessages) { - final remoteMessages = avmessages.state.data?.value; - if (remoteMessages == null) { - return; - } - // Add remote messages updates to queue to process asynchronously - _remoteMessagesQueue - .add(_MessageQueueEntry(remoteMessages: remoteMessages)); - } - - Future _updateRemoteMessagesStateAsync(_MessageQueueEntry entry) async { - final localMessagesCubit = _localMessagesCubit!; - - // Updated remote messages need to be merged with the local messages state - await localMessagesCubit.operate((shortArray) async { - // Ensure remoteMessages is sorted by timestamp - final remoteMessages = entry.remoteMessages - .sort((a, b) => a.timestamp.compareTo(b.timestamp)); - - // dedup? build local timestamp set? - - // Existing messages will always be sorted by timestamp so merging is easy - var localMessages = localMessagesCubit.state.state.data!.value; - - var pos = 0; - for (final newMessage in remoteMessages) { - var skip = false; - while (pos < localMessages.length) { - final m = localMessages[pos]; - pos++; - - // If timestamp to insert is less than - // the current position, insert it here - final newTs = Timestamp.fromInt64(newMessage.timestamp); - final curTs = Timestamp.fromInt64(m.timestamp); - final cmp = newTs.compareTo(curTs); - if (cmp < 0) { - break; - } else if (cmp == 0) { - skip = true; - break; - } - } - // Insert at this position - if (!skip) { - // Insert into dht backing array - await shortArray.tryInsertItem(pos, newMessage.writeToBuffer()); - // Insert into local copy as well for this operation - localMessages = localMessages.insert(pos, newMessage); - } - } - }); - } - - // Initialize local messages - static Future initLocalMessages({ - required ActiveAccountInfo activeAccountInfo, - required TypedKey remoteIdentityPublicKey, - required TypedKey localConversationKey, - required FutureOr Function(DHTShortArray) callback, - }) async { - final crypto = - await _makeMessagesCrypto(activeAccountInfo, remoteIdentityPublicKey); - final writer = activeAccountInfo.conversationWriter; - - return (await DHTShortArray.create( - parent: localConversationKey, crypto: crypto, smplWriter: writer)) - .deleteScope((messages) async => await callback(messages)); - } - - // Force refresh of messages - Future refresh() async { - final lcc = _localMessagesCubit; - final rcc = _remoteMessagesCubit; - - if (lcc != null) { - await lcc.refresh(); - } - if (rcc != null) { - await rcc.refresh(); - } - } - - Future addMessage({required proto.Message message}) async { - await _localMessagesCubit!.operate( - (shortArray) => shortArray.tryAddItem(message.writeToBuffer())); - } - - Future _getMessagesCrypto() async { - var messagesCrypto = _messagesCrypto; - if (messagesCrypto != null) { - return messagesCrypto; - } - messagesCrypto = - await _makeMessagesCrypto(_activeAccountInfo, _remoteIdentityPublicKey); - _messagesCrypto = messagesCrypto; - return messagesCrypto; - } - - static Future _makeMessagesCrypto( - ActiveAccountInfo activeAccountInfo, - TypedKey remoteIdentityPublicKey) async { - final identitySecret = activeAccountInfo.userLogin.identitySecret; - final cs = await Veilid.instance.getCryptoSystem(identitySecret.kind); - final sharedSecret = - await cs.cachedDH(remoteIdentityPublicKey.value, identitySecret.value); - - final messagesCrypto = await DHTRecordCryptoPrivate.fromSecret( - identitySecret.kind, sharedSecret); - return messagesCrypto; - } - - final ActiveAccountInfo _activeAccountInfo; - final TypedKey _remoteIdentityPublicKey; - DHTShortArrayCubit? _localMessagesCubit; - DHTShortArrayCubit? _remoteMessagesCubit; - final StreamController<_MessageQueueEntry> _remoteMessagesQueue; - StreamSubscription>>>? - _localSubscription; - StreamSubscription>>>? - _remoteSubscription; - // - DHTRecordCrypto? _messagesCrypto; -} diff --git a/lib/chat/cubits/single_contact_messages_cubit.dart b/lib/chat/cubits/single_contact_messages_cubit.dart new file mode 100644 index 0000000..cae7b80 --- /dev/null +++ b/lib/chat/cubits/single_contact_messages_cubit.dart @@ -0,0 +1,273 @@ +import 'dart:async'; + +import 'package:async_tools/async_tools.dart'; +import 'package:bloc_tools/bloc_tools.dart'; +import 'package:fast_immutable_collections/fast_immutable_collections.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:veilid_support/veilid_support.dart'; + +import '../../account_manager/account_manager.dart'; +import '../../proto/proto.dart' as proto; + +class _SingleContactMessageQueueEntry { + _SingleContactMessageQueueEntry({this.localMessages, this.remoteMessages}); + IList? localMessages; + IList? remoteMessages; +} + +typedef SingleContactMessagesState = AsyncValue>; + +// Cubit that processes single-contact chats +// Builds the reconciled chat record from the local and remote conversation keys +class SingleContactMessagesCubit extends Cubit { + SingleContactMessagesCubit({ + required ActiveAccountInfo activeAccountInfo, + required TypedKey remoteIdentityPublicKey, + required TypedKey localConversationRecordKey, + required TypedKey localMessagesRecordKey, + required TypedKey remoteConversationRecordKey, + required TypedKey remoteMessagesRecordKey, + required OwnedDHTRecordPointer reconciledChatRecord, + }) : _activeAccountInfo = activeAccountInfo, + _remoteIdentityPublicKey = remoteIdentityPublicKey, + _localConversationRecordKey = localConversationRecordKey, + _localMessagesRecordKey = localMessagesRecordKey, + _remoteConversationRecordKey = remoteConversationRecordKey, + _remoteMessagesRecordKey = remoteMessagesRecordKey, + _reconciledChatRecord = reconciledChatRecord, + _messagesUpdateQueue = StreamController(), + super(const AsyncValue.loading()) { + // Async Init + Future.delayed(Duration.zero, _init); + } + + @override + Future close() async { + await _messagesUpdateQueue.close(); + await _localSubscription?.cancel(); + await _remoteSubscription?.cancel(); + await _reconciledChatSubscription?.cancel(); + await _localMessagesCubit?.close(); + await _remoteMessagesCubit?.close(); + await _reconciledChatMessagesCubit?.close(); + await super.close(); + } + + // Initialize everything + Future _init() async { + // Make crypto + await _initMessagesCrypto(); + + // Reconciled messages key + await _initReconciledChatMessages(); + + // Local messages key + await _initLocalMessages(); + + // Remote messages key + await _initRemoteMessages(); + + // Messages listener + Future.delayed(Duration.zero, () async { + await for (final entry in _messagesUpdateQueue.stream) { + await _updateMessagesStateAsync(entry); + } + }); + } + + // Make crypto + + Future _initMessagesCrypto() async { + _messagesCrypto = await _activeAccountInfo + .makeConversationCrypto(_remoteIdentityPublicKey); + } + + // Open local messages key + Future _initLocalMessages() async { + final writer = _activeAccountInfo.conversationWriter; + + _localMessagesCubit = DHTShortArrayCubit( + open: () async => DHTShortArray.openWrite( + _localMessagesRecordKey, writer, + parent: _localConversationRecordKey, crypto: _messagesCrypto), + decodeElement: proto.Message.fromBuffer); + _localSubscription = + _localMessagesCubit!.stream.listen(_updateLocalMessagesState); + _updateLocalMessagesState(_localMessagesCubit!.state); + } + + // Open remote messages key + Future _initRemoteMessages() async { + _remoteMessagesCubit = DHTShortArrayCubit( + open: () async => DHTShortArray.openRead(_remoteMessagesRecordKey, + parent: _remoteConversationRecordKey, crypto: _messagesCrypto), + decodeElement: proto.Message.fromBuffer); + _remoteSubscription = + _remoteMessagesCubit!.stream.listen(_updateRemoteMessagesState); + _updateRemoteMessagesState(_remoteMessagesCubit!.state); + } + + // Open reconciled chat record key + Future _initReconciledChatMessages() async { + final accountRecordKey = + _activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey; + + _reconciledChatMessagesCubit = DHTShortArrayCubit( + open: () async => DHTShortArray.openOwned(_reconciledChatRecord, + parent: accountRecordKey), + decodeElement: proto.Message.fromBuffer); + _reconciledChatSubscription = + _reconciledChatMessagesCubit!.stream.listen(_updateReconciledChatState); + _updateReconciledChatState(_reconciledChatMessagesCubit!.state); + } + + // Called when the local messages list gets a change + void _updateLocalMessagesState( + BlocBusyState>> avmessages) { + final localMessages = avmessages.state.data?.value; + if (localMessages == null) { + return; + } + // Add local messages updates to queue to process asynchronously + _messagesUpdateQueue + .add(_SingleContactMessageQueueEntry(localMessages: localMessages)); + } + + // Called when the remote messages list gets a change + void _updateRemoteMessagesState( + BlocBusyState>> avmessages) { + final remoteMessages = avmessages.state.data?.value; + if (remoteMessages == null) { + return; + } + // Add remote messages updates to queue to process asynchronously + _messagesUpdateQueue + .add(_SingleContactMessageQueueEntry(remoteMessages: remoteMessages)); + } + + // Called when the reconciled messages list gets a change + void _updateReconciledChatState( + BlocBusyState>> avmessages) { + // When reconciled messages are updated, pass this + // directly to the messages cubit state + emit(avmessages.state); + } + + Future _mergeMessagesInner( + {required DHTShortArray reconciledMessages, + required IList messages}) async { + // Ensure remoteMessages is sorted by timestamp + final newMessages = messages + .sort((a, b) => a.timestamp.compareTo(b.timestamp)) + .removeDuplicates(); + + // Existing messages will always be sorted by timestamp so merging is easy + final existingMessages = + _reconciledChatMessagesCubit!.state.state.data!.value.toList(); + + var ePos = 0; + var nPos = 0; + while (ePos < existingMessages.length && nPos < newMessages.length) { + final existingMessage = existingMessages[ePos]; + final newMessage = newMessages[nPos]; + + // If timestamp to insert is less than + // the current position, insert it here + final newTs = Timestamp.fromInt64(newMessage.timestamp); + final existingTs = Timestamp.fromInt64(existingMessage.timestamp); + final cmp = newTs.compareTo(existingTs); + if (cmp < 0) { + // New message belongs here + + // Insert into dht backing array + await reconciledMessages.tryInsertItem( + ePos, newMessage.writeToBuffer()); + // Insert into local copy as well for this operation + existingMessages.insert(ePos, newMessage); + + // Next message + nPos++; + ePos++; + } else if (cmp == 0) { + // Duplicate, skip + nPos++; + ePos++; + } else if (cmp > 0) { + // New message belongs later + ePos++; + } + } + // If there are any new messages left, append them all + while (nPos < newMessages.length) { + final newMessage = newMessages[nPos]; + + // Append to dht backing array + await reconciledMessages.tryAddItem(newMessage.writeToBuffer()); + // Insert into local copy as well for this operation + existingMessages.add(newMessage); + + nPos++; + } + } + + Future _updateMessagesStateAsync( + _SingleContactMessageQueueEntry entry) async { + final reconciledChatMessagesCubit = _reconciledChatMessagesCubit!; + + // Merge remote and local messages into the reconciled chat log + await reconciledChatMessagesCubit.operate((reconciledMessages) async { + // xxx for now, keep two lists, but can probable simplify this out soon + if (entry.localMessages != null) { + await _mergeMessagesInner( + reconciledMessages: reconciledMessages, + messages: entry.localMessages!); + } + if (entry.remoteMessages != null) { + await _mergeMessagesInner( + reconciledMessages: reconciledMessages, + messages: entry.remoteMessages!); + } + }); + } + + // Force refresh of messages + Future refresh() async { + final lcc = _localMessagesCubit; + final rcc = _remoteMessagesCubit; + + if (lcc != null) { + await lcc.refresh(); + } + if (rcc != null) { + await rcc.refresh(); + } + } + + Future addMessage({required proto.Message message}) async { + await _localMessagesCubit!.operate( + (shortArray) => shortArray.tryAddItem(message.writeToBuffer())); + } + + final ActiveAccountInfo _activeAccountInfo; + final TypedKey _remoteIdentityPublicKey; + final TypedKey _localConversationRecordKey; + final TypedKey _localMessagesRecordKey; + final TypedKey _remoteConversationRecordKey; + final TypedKey _remoteMessagesRecordKey; + final OwnedDHTRecordPointer _reconciledChatRecord; + + late final DHTRecordCrypto _messagesCrypto; + + DHTShortArrayCubit? _localMessagesCubit; + DHTShortArrayCubit? _remoteMessagesCubit; + DHTShortArrayCubit? _reconciledChatMessagesCubit; + + final StreamController<_SingleContactMessageQueueEntry> _messagesUpdateQueue; + + StreamSubscription>>>? + _localSubscription; + StreamSubscription>>>? + _remoteSubscription; + StreamSubscription>>>? + _reconciledChatSubscription; +} diff --git a/lib/chat/views/chat_component.dart b/lib/chat/views/chat_component.dart index 8d829bb..146185b 100644 --- a/lib/chat/views/chat_component.dart +++ b/lib/chat/views/chat_component.dart @@ -19,8 +19,8 @@ import '../chat.dart'; class ChatComponent extends StatelessWidget { const ChatComponent._( {required TypedKey localUserIdentityKey, - required MessagesCubit messagesCubit, - required MessagesState messagesState, + required SingleContactMessagesCubit messagesCubit, + required SingleContactMessagesState messagesState, required types.User localUser, required types.User remoteUser, super.key}) @@ -31,8 +31,8 @@ class ChatComponent extends StatelessWidget { _remoteUser = remoteUser; final TypedKey _localUserIdentityKey; - final MessagesCubit _messagesCubit; - final MessagesState _messagesState; + final SingleContactMessagesCubit _messagesCubit; + final SingleContactMessagesState _messagesState; final types.User _localUser; final types.User _remoteUser; @@ -78,8 +78,8 @@ class ChatComponent extends StatelessWidget { firstName: editedName); // Get the messages cubit - final messages = context.select( + final messages = context.select( (x) => x.tryOperate(remoteConversationRecordKey, closure: (cubit) => (cubit, cubit.state))); diff --git a/lib/chat_list/cubits/active_conversation_messages_bloc_map_cubit.dart b/lib/chat_list/cubits/active_conversation_messages_bloc_map_cubit.dart deleted file mode 100644 index 3fd47f2..0000000 --- a/lib/chat_list/cubits/active_conversation_messages_bloc_map_cubit.dart +++ /dev/null @@ -1,68 +0,0 @@ -import 'dart:async'; - -import 'package:async_tools/async_tools.dart'; -import 'package:bloc_tools/bloc_tools.dart'; -import 'package:fast_immutable_collections/fast_immutable_collections.dart'; -import 'package:veilid_support/veilid_support.dart'; - -import '../../account_manager/account_manager.dart'; -import '../../chat/chat.dart'; -import '../../proto/proto.dart' as proto; -import 'active_conversations_bloc_map_cubit.dart'; - -// Map of remoteConversationRecordKey to MessagesCubit -// Wraps a MessagesCubit to stream the latest messages to the state -// Automatically follows the state of a ActiveConversationsBlocMapCubit. -class ActiveConversationMessagesBlocMapCubit extends BlocMapCubit>, MessagesCubit> - with - StateFollower> { - ActiveConversationMessagesBlocMapCubit({ - required ActiveAccountInfo activeAccountInfo, - }) : _activeAccountInfo = activeAccountInfo; - - Future _addConversationMessages( - {required proto.Contact contact, - required proto.Conversation localConversation, - required proto.Conversation remoteConversation}) async => - add(() => MapEntry( - contact.remoteConversationRecordKey.toVeilid(), - MessagesCubit( - activeAccountInfo: _activeAccountInfo, - remoteIdentityPublicKey: contact.identityPublicKey.toVeilid(), - localConversationRecordKey: - contact.localConversationRecordKey.toVeilid(), - remoteConversationRecordKey: - contact.remoteConversationRecordKey.toVeilid(), - localMessagesRecordKey: localConversation.messages.toVeilid(), - remoteMessagesRecordKey: - remoteConversation.messages.toVeilid()))); - - /// StateFollower ///////////////////////// - - @override - IMap> getStateMap( - ActiveConversationsBlocMapState state) => - state; - - @override - Future removeFromState(TypedKey key) => remove(key); - - @override - Future updateState( - TypedKey key, AsyncValue value) async { - await value.when( - data: (state) => _addConversationMessages( - contact: state.contact, - localConversation: state.localConversation, - remoteConversation: state.remoteConversation), - loading: () => addState(key, const AsyncValue.loading()), - error: (error, stackTrace) => - addState(key, AsyncValue.error(error, stackTrace))); - } - - //// - - final ActiveAccountInfo _activeAccountInfo; -} diff --git a/lib/chat_list/cubits/active_conversations_bloc_map_cubit.dart b/lib/chat_list/cubits/active_conversations_bloc_map_cubit.dart index 8212331..8ce919a 100644 --- a/lib/chat_list/cubits/active_conversations_bloc_map_cubit.dart +++ b/lib/chat_list/cubits/active_conversations_bloc_map_cubit.dart @@ -34,6 +34,9 @@ typedef ActiveConversationsBlocMapState // Map of remoteConversationRecordKey to ActiveConversationCubit // Wraps a conversation cubit to only expose completely built conversations // Automatically follows the state of a ChatListCubit. +// Even though 'conversations' are per-contact and not per-chat +// We currently only build the cubits for the chats that are active, not +// archived chats or contacts that are not actively in a chat. class ActiveConversationsBlocMapCubit extends BlocMapCubit, ActiveConversationCubit> with @@ -82,7 +85,7 @@ class ActiveConversationsBlocMapCubit extends BlocMapCubit e.remoteConversationKey.toVeilid(), + keyMapper: (e) => e.remoteConversationRecordKey.toVeilid(), valueMapper: (e) => e); } @@ -99,7 +102,7 @@ class ActiveConversationsBlocMapCubit extends BlocMapCubit c.remoteConversationRecordKey.toVeilid() == key); if (contactIndex == -1) { - await addState(key, AsyncValue.error('Contact not found for chat')); + await addState(key, AsyncValue.error('Contact not found')); return; } final contact = contactList[contactIndex]; diff --git a/lib/chat_list/cubits/active_single_contact_chat_bloc_map_cubit.dart b/lib/chat_list/cubits/active_single_contact_chat_bloc_map_cubit.dart new file mode 100644 index 0000000..c6827f3 --- /dev/null +++ b/lib/chat_list/cubits/active_single_contact_chat_bloc_map_cubit.dart @@ -0,0 +1,108 @@ +import 'dart:async'; + +import 'package:async_tools/async_tools.dart'; +import 'package:bloc_tools/bloc_tools.dart'; +import 'package:fast_immutable_collections/fast_immutable_collections.dart'; +import 'package:veilid_support/veilid_support.dart'; + +import '../../account_manager/account_manager.dart'; +import '../../chat/chat.dart'; +import '../../contacts/contacts.dart'; +import '../../proto/proto.dart' as proto; +import 'active_conversations_bloc_map_cubit.dart'; +import 'chat_list_cubit.dart'; + +// Map of remoteConversationRecordKey to MessagesCubit +// Wraps a MessagesCubit to stream the latest messages to the state +// Automatically follows the state of a ActiveConversationsBlocMapCubit. +class ActiveSingleContactChatBlocMapCubit extends BlocMapCubit>, SingleContactMessagesCubit> + with + StateFollower> { + ActiveSingleContactChatBlocMapCubit( + {required ActiveAccountInfo activeAccountInfo, + required ContactListCubit contactListCubit, + required ChatListCubit chatListCubit}) + : _activeAccountInfo = activeAccountInfo, + _contactListCubit = contactListCubit, + _chatListCubit = chatListCubit; + + Future _addConversationMessages( + {required proto.Contact contact, + required proto.Chat chat, + required proto.Conversation localConversation, + required proto.Conversation remoteConversation}) async => + add(() => MapEntry( + contact.remoteConversationRecordKey.toVeilid(), + SingleContactMessagesCubit( + activeAccountInfo: _activeAccountInfo, + remoteIdentityPublicKey: contact.identityPublicKey.toVeilid(), + localConversationRecordKey: + contact.localConversationRecordKey.toVeilid(), + remoteConversationRecordKey: + contact.remoteConversationRecordKey.toVeilid(), + localMessagesRecordKey: localConversation.messages.toVeilid(), + remoteMessagesRecordKey: remoteConversation.messages.toVeilid(), + reconciledChatRecord: chat.reconciledChatRecord.toVeilid(), + ))); + + /// StateFollower ///////////////////////// + + @override + IMap> getStateMap( + ActiveConversationsBlocMapState state) => + state; + + @override + Future removeFromState(TypedKey key) => remove(key); + + @override + Future updateState( + TypedKey key, AsyncValue value) async { + // Get the contact object for this single contact chat + final contactList = _contactListCubit.state.state.data?.value; + if (contactList == null) { + await addState(key, const AsyncValue.loading()); + return; + } + final contactIndex = contactList + .indexWhere((c) => c.remoteConversationRecordKey.toVeilid() == key); + if (contactIndex == -1) { + await addState( + key, AsyncValue.error('Contact not found for conversation')); + return; + } + final contact = contactList[contactIndex]; + + // Get the chat object for this single contact chat + final chatList = _chatListCubit.state.state.data?.value; + if (chatList == null) { + await addState(key, const AsyncValue.loading()); + return; + } + final chatIndex = chatList + .indexWhere((c) => c.remoteConversationRecordKey.toVeilid() == key); + if (contactIndex == -1) { + await addState(key, AsyncValue.error('Chat not found for conversation')); + return; + } + final chat = chatList[chatIndex]; + + await value.when( + data: (state) => _addConversationMessages( + contact: contact, + chat: chat, + localConversation: state.localConversation, + remoteConversation: state.remoteConversation), + loading: () => addState(key, const AsyncValue.loading()), + error: (error, stackTrace) => + addState(key, AsyncValue.error(error, stackTrace))); + } + + //// + + final ActiveAccountInfo _activeAccountInfo; + final ContactListCubit _contactListCubit; + final ChatListCubit _chatListCubit; +} diff --git a/lib/chat_list/cubits/chat_list_cubit.dart b/lib/chat_list/cubits/chat_list_cubit.dart index 286f03b..420dfbf 100644 --- a/lib/chat_list/cubits/chat_list_cubit.dart +++ b/lib/chat_list/cubits/chat_list_cubit.dart @@ -5,6 +5,7 @@ import 'package:veilid_support/veilid_support.dart'; import '../../account_manager/account_manager.dart'; import '../../chat/chat.dart'; import '../../proto/proto.dart' as proto; +import '../../tools/tools.dart'; ////////////////////////////////////////////////// @@ -16,7 +17,8 @@ class ChatListCubit extends DHTShortArrayCubit { required ActiveAccountInfo activeAccountInfo, required proto.Account account, required this.activeChatCubit, - }) : super( + }) : _activeAccountInfo = activeAccountInfo, + super( open: () => _open(activeAccountInfo, account), decodeElement: proto.Chat.fromBuffer); @@ -50,15 +52,24 @@ class ChatListCubit extends DHTShortArrayCubit { throw Exception('Failed to get chat'); } final c = proto.Chat.fromBuffer(cbuf); - if (c.remoteConversationKey == remoteConversationRecordKeyProto) { + if (c.remoteConversationRecordKey == remoteConversationRecordKeyProto) { // Nothing to do here return; } } + final accountRecordKey = _activeAccountInfo + .userLogin.accountRecordInfo.accountRecord.recordKey; + + // Make a record that can store the reconciled version of the chat + final reconciledChatRecord = + await (await DHTShortArray.create(parent: accountRecordKey)) + .scope((r) async => r.recordPointer); + // Create conversation type Chat final chat = proto.Chat() ..type = proto.ChatType.SINGLE_CONTACT - ..remoteConversationKey = remoteConversationRecordKeyProto; + ..remoteConversationRecordKey = remoteConversationRecordKeyProto + ..reconciledChatRecord = reconciledChatRecord.toProto(); // Add chat final added = await shortArray.tryAddItem(chat.writeToBuffer()); @@ -71,8 +82,9 @@ class ChatListCubit extends DHTShortArrayCubit { /// Delete a chat Future deleteChat( {required TypedKey remoteConversationRecordKey}) async { - // Create conversation type Chat final remoteConversationKey = remoteConversationRecordKey.toProto(); + final accountRecordKey = + _activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey; // Remove Chat from account's list // if this fails, don't keep retrying, user can try again later @@ -86,8 +98,18 @@ class ChatListCubit extends DHTShortArrayCubit { throw Exception('Failed to get chat'); } final c = proto.Chat.fromBuffer(cbuf); - if (c.remoteConversationKey == remoteConversationKey) { - await shortArray.tryRemoveItem(i); + if (c.remoteConversationRecordKey == remoteConversationKey) { + // Found the right chat + if (await shortArray.tryRemoveItem(i) != null) { + try { + await (await DHTShortArray.openOwned( + c.reconciledChatRecord.toVeilid(), + parent: accountRecordKey)) + .delete(); + } on Exception catch (e) { + log.debug('error removing reconciled chat record: $e', e); + } + } return; } } @@ -95,4 +117,5 @@ class ChatListCubit extends DHTShortArrayCubit { } final ActiveChatCubit activeChatCubit; + final ActiveAccountInfo _activeAccountInfo; } diff --git a/lib/chat_list/cubits/cubits.dart b/lib/chat_list/cubits/cubits.dart index 0f099ca..35595db 100644 --- a/lib/chat_list/cubits/cubits.dart +++ b/lib/chat_list/cubits/cubits.dart @@ -1,3 +1,3 @@ -export 'active_conversation_messages_bloc_map_cubit.dart'; +export 'active_single_contact_chat_bloc_map_cubit.dart'; export 'active_conversations_bloc_map_cubit.dart'; export 'chat_list_cubit.dart'; diff --git a/lib/chat_list/views/chat_single_contact_list_widget.dart b/lib/chat_list/views/chat_single_contact_list_widget.dart index 5952dc4..04092b4 100644 --- a/lib/chat_list/views/chat_single_contact_list_widget.dart +++ b/lib/chat_list/views/chat_single_contact_list_widget.dart @@ -40,7 +40,7 @@ class ChatSingleContactListWidget extends StatelessWidget { initialList: chatList.toList(), builder: (l, i, c) { final contact = - contactMap[c.remoteConversationKey]; + contactMap[c.remoteConversationRecordKey]; if (contact == null) { return const Text('...'); } @@ -52,7 +52,7 @@ class ChatSingleContactListWidget extends StatelessWidget { final lowerValue = value.toLowerCase(); return chatList.where((c) { final contact = - contactMap[c.remoteConversationKey]; + contactMap[c.remoteConversationRecordKey]; if (contact == null) { return false; } diff --git a/lib/contact_invitation/cubits/cubits.dart b/lib/contact_invitation/cubits/cubits.dart index c55e119..fd2833f 100644 --- a/lib/contact_invitation/cubits/cubits.dart +++ b/lib/contact_invitation/cubits/cubits.dart @@ -1,4 +1,5 @@ export 'contact_invitation_list_cubit.dart'; export 'contact_request_inbox_cubit.dart'; +export 'invitation_generator_cubit.dart'; export 'waiting_invitation_cubit.dart'; export 'waiting_invitations_bloc_map_cubit.dart'; diff --git a/lib/contact_invitation/cubits/invitation_generator_cubit.dart b/lib/contact_invitation/cubits/invitation_generator_cubit.dart new file mode 100644 index 0000000..c6f7258 --- /dev/null +++ b/lib/contact_invitation/cubits/invitation_generator_cubit.dart @@ -0,0 +1,8 @@ +import 'dart:typed_data'; + +import 'package:bloc_tools/bloc_tools.dart'; + +class InvitationGeneratorCubit extends FutureCubit { + InvitationGeneratorCubit(super.fut); + InvitationGeneratorCubit.value(super.v) : super.value(); +} diff --git a/lib/contact_invitation/views/contact_invitation_display.dart b/lib/contact_invitation/views/contact_invitation_display.dart index 33036e7..4291541 100644 --- a/lib/contact_invitation/views/contact_invitation_display.dart +++ b/lib/contact_invitation/views/contact_invitation_display.dart @@ -2,7 +2,6 @@ import 'dart:math'; import 'package:awesome_extensions/awesome_extensions.dart'; import 'package:basic_utils/basic_utils.dart'; -import 'package:bloc_tools/bloc_tools.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -12,10 +11,7 @@ import 'package:qr_flutter/qr_flutter.dart'; import 'package:veilid_support/veilid_support.dart'; import '../../tools/tools.dart'; - -class InvitationGeneratorCubit extends FutureCubit { - InvitationGeneratorCubit(super.fut); -} +import '../contact_invitation.dart'; class ContactInvitationDisplayDialog extends StatefulWidget { const ContactInvitationDisplayDialog({ diff --git a/lib/contact_invitation/views/contact_invitation_item_widget.dart b/lib/contact_invitation/views/contact_invitation_item_widget.dart index fee8629..e633390 100644 --- a/lib/contact_invitation/views/contact_invitation_item_widget.dart +++ b/lib/contact_invitation/views/contact_invitation_item_widget.dart @@ -108,9 +108,9 @@ class ContactInvitationItemWidget extends StatelessWidget { await showDialog( context: context, builder: (context) => BlocProvider( - create: (context) => InvitationGeneratorCubit( - Future.value(Uint8List.fromList( - contactInvitationRecord.invitation))), + create: (context) => InvitationGeneratorCubit + .value(Uint8List.fromList( + contactInvitationRecord.invitation)), child: ContactInvitationDisplayDialog( message: contactInvitationRecord.message, ))); diff --git a/lib/contacts/cubits/conversation_cubit.dart b/lib/contacts/cubits/conversation_cubit.dart index 8994209..23c61c6 100644 --- a/lib/contacts/cubits/conversation_cubit.dart +++ b/lib/contacts/cubits/conversation_cubit.dart @@ -12,7 +12,6 @@ import 'package:meta/meta.dart'; import 'package:veilid_support/veilid_support.dart'; import '../../account_manager/account_manager.dart'; -import '../../chat/chat.dart'; import '../../proto/proto.dart' as proto; @immutable @@ -47,7 +46,7 @@ class ConversationCubit extends Cubit> { // Open local record key if it is specified final pool = DHTRecordPool.instance; - final crypto = await getConversationCrypto(); + final crypto = await _cachedConversationCrypto(); final writer = _activeAccountInfo.conversationWriter; final record = await pool.openWrite( _localConversationRecordKey!, writer, @@ -63,7 +62,7 @@ class ConversationCubit extends Cubit> { // Open remote record key if it is specified final pool = DHTRecordPool.instance; - final crypto = await getConversationCrypto(); + final crypto = await _cachedConversationCrypto(); final record = await pool.openRead(_remoteConversationRecordKey, parent: accountRecordKey, crypto: crypto); await _setRemoteConversation(record); @@ -163,7 +162,7 @@ class ConversationCubit extends Cubit> { final accountRecordKey = _activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey; - final crypto = await getConversationCrypto(); + final crypto = await _cachedConversationCrypto(); final writer = _activeAccountInfo.conversationWriter; // Open with SMPL scheme for identity writer @@ -187,7 +186,7 @@ class ConversationCubit extends Cubit> { // ignore: prefer_expression_function_bodies .deleteScope((localConversation) async { // Make messages log - return MessagesCubit.initLocalMessages( + return initLocalMessages( activeAccountInfo: _activeAccountInfo, remoteIdentityPublicKey: _remoteIdentityPublicKey, localConversationKey: localConversation.key, @@ -197,7 +196,7 @@ class ConversationCubit extends Cubit> { ..profile = profile ..identityMasterJson = jsonEncode( _activeAccountInfo.localAccount.identityMaster.toJson()) - ..messages = messages.record.key.toProto(); + ..messages = messages.recordKey.toProto(); // Write initial conversation to record final update = await localConversation.tryWriteProtobuf( @@ -221,6 +220,22 @@ class ConversationCubit extends Cubit> { return out; } + // Initialize local messages + Future initLocalMessages({ + required ActiveAccountInfo activeAccountInfo, + required TypedKey remoteIdentityPublicKey, + required TypedKey localConversationKey, + required FutureOr Function(DHTShortArray) callback, + }) async { + final crypto = + await activeAccountInfo.makeConversationCrypto(remoteIdentityPublicKey); + final writer = activeAccountInfo.conversationWriter; + + return (await DHTShortArray.create( + parent: localConversationKey, crypto: crypto, smplWriter: writer)) + .deleteScope((messages) async => await callback(messages)); + } + // Force refresh of conversation keys Future refresh() async { final lcc = _localConversationCubit; @@ -247,18 +262,14 @@ class ConversationCubit extends Cubit> { return update; } - Future getConversationCrypto() async { + Future _cachedConversationCrypto() async { var conversationCrypto = _conversationCrypto; if (conversationCrypto != null) { return conversationCrypto; } - final identitySecret = _activeAccountInfo.userLogin.identitySecret; - final cs = await Veilid.instance.getCryptoSystem(identitySecret.kind); - final sharedSecret = - await cs.cachedDH(_remoteIdentityPublicKey.value, identitySecret.value); + conversationCrypto = await _activeAccountInfo + .makeConversationCrypto(_remoteIdentityPublicKey); - conversationCrypto = await DHTRecordCryptoPrivate.fromSecret( - identitySecret.kind, sharedSecret); _conversationCrypto = conversationCrypto; return conversationCrypto; } diff --git a/lib/layout/home/home_account_ready/home_account_ready_shell.dart b/lib/layout/home/home_account_ready/home_account_ready_shell.dart index 1f27fce..1ccf562 100644 --- a/lib/layout/home/home_account_ready/home_account_ready_shell.dart +++ b/lib/layout/home/home_account_ready/home_account_ready_shell.dart @@ -151,11 +151,12 @@ class HomeAccountReadyShellState extends State { contactListCubit: context.read()) ..followBloc(context.read())), BlocProvider( - create: (context) => - ActiveConversationMessagesBlocMapCubit( - activeAccountInfo: widget.activeAccountInfo, - )..followBloc( - context.read())), + create: (context) => ActiveSingleContactChatBlocMapCubit( + activeAccountInfo: widget.activeAccountInfo, + contactListCubit: context.read(), + chatListCubit: context.read()) + ..followBloc( + context.read())), BlocProvider( create: (context) => WaitingInvitationsBlocMapCubit( activeAccountInfo: widget.activeAccountInfo, diff --git a/lib/proto/veilidchat.pb.dart b/lib/proto/veilidchat.pb.dart index a0db194..1503c57 100644 --- a/lib/proto/veilidchat.pb.dart +++ b/lib/proto/veilidchat.pb.dart @@ -456,7 +456,8 @@ class Chat extends $pb.GeneratedMessage { static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'Chat', package: const $pb.PackageName(_omitMessageNames ? '' : 'veilidchat'), createEmptyInstance: create) ..e(1, _omitFieldNames ? '' : 'type', $pb.PbFieldType.OE, defaultOrMaker: ChatType.CHAT_TYPE_UNSPECIFIED, valueOf: ChatType.valueOf, enumValues: ChatType.values) - ..aOM<$1.TypedKey>(2, _omitFieldNames ? '' : 'remoteConversationKey', subBuilder: $1.TypedKey.create) + ..aOM<$1.TypedKey>(2, _omitFieldNames ? '' : 'remoteConversationRecordKey', subBuilder: $1.TypedKey.create) + ..aOM<$0.OwnedDHTRecordPointer>(3, _omitFieldNames ? '' : 'reconciledChatRecord', subBuilder: $0.OwnedDHTRecordPointer.create) ..hasRequiredFields = false ; @@ -491,15 +492,26 @@ class Chat extends $pb.GeneratedMessage { void clearType() => clearField(1); @$pb.TagNumber(2) - $1.TypedKey get remoteConversationKey => $_getN(1); + $1.TypedKey get remoteConversationRecordKey => $_getN(1); @$pb.TagNumber(2) - set remoteConversationKey($1.TypedKey v) { setField(2, v); } + set remoteConversationRecordKey($1.TypedKey v) { setField(2, v); } @$pb.TagNumber(2) - $core.bool hasRemoteConversationKey() => $_has(1); + $core.bool hasRemoteConversationRecordKey() => $_has(1); @$pb.TagNumber(2) - void clearRemoteConversationKey() => clearField(2); + void clearRemoteConversationRecordKey() => clearField(2); @$pb.TagNumber(2) - $1.TypedKey ensureRemoteConversationKey() => $_ensure(1); + $1.TypedKey ensureRemoteConversationRecordKey() => $_ensure(1); + + @$pb.TagNumber(3) + $0.OwnedDHTRecordPointer get reconciledChatRecord => $_getN(2); + @$pb.TagNumber(3) + set reconciledChatRecord($0.OwnedDHTRecordPointer v) { setField(3, v); } + @$pb.TagNumber(3) + $core.bool hasReconciledChatRecord() => $_has(2); + @$pb.TagNumber(3) + void clearReconciledChatRecord() => clearField(3); + @$pb.TagNumber(3) + $0.OwnedDHTRecordPointer ensureReconciledChatRecord() => $_ensure(2); } class Account extends $pb.GeneratedMessage { diff --git a/lib/proto/veilidchat.pbjson.dart b/lib/proto/veilidchat.pbjson.dart index c8eb2c8..7aea1fb 100644 --- a/lib/proto/veilidchat.pbjson.dart +++ b/lib/proto/veilidchat.pbjson.dart @@ -185,15 +185,17 @@ const Chat$json = { '1': 'Chat', '2': [ {'1': 'type', '3': 1, '4': 1, '5': 14, '6': '.veilidchat.ChatType', '10': 'type'}, - {'1': 'remote_conversation_key', '3': 2, '4': 1, '5': 11, '6': '.veilid.TypedKey', '10': 'remoteConversationKey'}, + {'1': 'remote_conversation_record_key', '3': 2, '4': 1, '5': 11, '6': '.veilid.TypedKey', '10': 'remoteConversationRecordKey'}, + {'1': 'reconciled_chat_record', '3': 3, '4': 1, '5': 11, '6': '.dht.OwnedDHTRecordPointer', '10': 'reconciledChatRecord'}, ], }; /// Descriptor for `Chat`. Decode as a `google.protobuf.DescriptorProto`. final $typed_data.Uint8List chatDescriptor = $convert.base64Decode( - 'CgRDaGF0EigKBHR5cGUYASABKA4yFC52ZWlsaWRjaGF0LkNoYXRUeXBlUgR0eXBlEkgKF3JlbW' - '90ZV9jb252ZXJzYXRpb25fa2V5GAIgASgLMhAudmVpbGlkLlR5cGVkS2V5UhVyZW1vdGVDb252' - 'ZXJzYXRpb25LZXk='); + 'CgRDaGF0EigKBHR5cGUYASABKA4yFC52ZWlsaWRjaGF0LkNoYXRUeXBlUgR0eXBlElUKHnJlbW' + '90ZV9jb252ZXJzYXRpb25fcmVjb3JkX2tleRgCIAEoCzIQLnZlaWxpZC5UeXBlZEtleVIbcmVt' + 'b3RlQ29udmVyc2F0aW9uUmVjb3JkS2V5ElAKFnJlY29uY2lsZWRfY2hhdF9yZWNvcmQYAyABKA' + 'syGi5kaHQuT3duZWRESFRSZWNvcmRQb2ludGVyUhRyZWNvbmNpbGVkQ2hhdFJlY29yZA=='); @$core.Deprecated('Use accountDescriptor instead') const Account$json = { diff --git a/lib/proto/veilidchat.proto b/lib/proto/veilidchat.proto index 8e5f231..1727ee1 100644 --- a/lib/proto/veilidchat.proto +++ b/lib/proto/veilidchat.proto @@ -42,8 +42,12 @@ message Message { repeated Attachment attachments = 5; } -// A record of a 1-1 chat that is synchronized between -// two users. Visible and encrypted for the other party +// The means of direct communications that is synchronized between +// two users. Visible and encrypted for the other party. +// Includes communications for: +// * Profile changes +// * Identity changes +// * 1-1 chat messages // // DHT Schema: SMPL(0,1,[identityPublicKey]) // DHT Key (UnicastOutbox): localConversation @@ -117,12 +121,15 @@ enum ChatType { GROUP = 2; } -// Either a 1-1 converation or a group chat (eventually) +// Either a 1-1 conversation or a group chat (eventually) +// Privately encrypted, this is the local user's copy of the chat message Chat { // What kind of chat is this ChatType type = 1; - // 1-1 Chat key - veilid.TypedKey remote_conversation_key = 2; + // Conversation key for the other party + veilid.TypedKey remote_conversation_record_key = 2; + // Reconciled chat record DHTLog (xxx for now DHTShortArray) + dht.OwnedDHTRecordPointer reconciled_chat_record = 3; } // A record of an individual account diff --git a/packages/bloc_tools/lib/src/future_cubit.dart b/packages/bloc_tools/lib/src/future_cubit.dart index 77d8387..b14ac72 100644 --- a/packages/bloc_tools/lib/src/future_cubit.dart +++ b/packages/bloc_tools/lib/src/future_cubit.dart @@ -12,4 +12,5 @@ abstract class FutureCubit extends Cubit> { emit(AsyncValue.error(e, stackTrace)); })); } + FutureCubit.value(State state) : super(AsyncValue.data(state)); } diff --git a/packages/veilid_support/lib/dht_support/dht_support.dart b/packages/veilid_support/lib/dht_support/dht_support.dart index 24f584f..869a267 100644 --- a/packages/veilid_support/lib/dht_support/dht_support.dart +++ b/packages/veilid_support/lib/dht_support/dht_support.dart @@ -2,8 +2,5 @@ library dht_support; -export 'src/dht_record_crypto.dart'; -export 'src/dht_record_cubit.dart'; -export 'src/dht_record_pool.dart'; -export 'src/dht_short_array.dart'; -export 'src/dht_short_array_cubit.dart'; +export 'src/dht_record/barrel.dart'; +export 'src/dht_short_array/barrel.dart'; diff --git a/packages/veilid_support/lib/dht_support/proto/dht.proto b/packages/veilid_support/lib/dht_support/proto/dht.proto index 9ad53b6..087cc9c 100644 --- a/packages/veilid_support/lib/dht_support/proto/dht.proto +++ b/packages/veilid_support/lib/dht_support/proto/dht.proto @@ -42,6 +42,10 @@ message DHTShortArray { // key = idx / stride // subkey = idx % stride bytes index = 2; + + // Most recent sequence numbers for elements + repeated uint32 seqs = 3; + // Free items are not represented in the list but can be // calculated through iteration } diff --git a/packages/veilid_support/lib/dht_support/src/dht_record/barrel.dart b/packages/veilid_support/lib/dht_support/src/dht_record/barrel.dart new file mode 100644 index 0000000..a1e3099 --- /dev/null +++ b/packages/veilid_support/lib/dht_support/src/dht_record/barrel.dart @@ -0,0 +1,3 @@ +export 'dht_record_crypto.dart'; +export 'dht_record_cubit.dart'; +export 'dht_record_pool.dart'; diff --git a/packages/veilid_support/lib/dht_support/src/dht_record.dart b/packages/veilid_support/lib/dht_support/src/dht_record/dht_record.dart similarity index 98% rename from packages/veilid_support/lib/dht_support/src/dht_record.dart rename to packages/veilid_support/lib/dht_support/src/dht_record/dht_record.dart index 20d0524..713b076 100644 --- a/packages/veilid_support/lib/dht_support/src/dht_record.dart +++ b/packages/veilid_support/lib/dht_support/src/dht_record/dht_record.dart @@ -347,6 +347,11 @@ class DHTRecord { } } + Future inspect( + {List? subkeys, + DHTReportScope scope = DHTReportScope.local}) => + _routingContext.inspectDHTRecord(key, subkeys: subkeys, scope: scope); + void _addValueChange( {required bool local, required Uint8List data, diff --git a/packages/veilid_support/lib/dht_support/src/dht_record_crypto.dart b/packages/veilid_support/lib/dht_support/src/dht_record/dht_record_crypto.dart similarity index 97% rename from packages/veilid_support/lib/dht_support/src/dht_record_crypto.dart rename to packages/veilid_support/lib/dht_support/src/dht_record/dht_record_crypto.dart index 60534b6..0e69078 100644 --- a/packages/veilid_support/lib/dht_support/src/dht_record_crypto.dart +++ b/packages/veilid_support/lib/dht_support/src/dht_record/dht_record_crypto.dart @@ -1,6 +1,6 @@ import 'dart:async'; import 'dart:typed_data'; -import '../../../../veilid_support.dart'; +import '../../../../../veilid_support.dart'; abstract class DHTRecordCrypto { Future encrypt(Uint8List data, int subkey); diff --git a/packages/veilid_support/lib/dht_support/src/dht_record_cubit.dart b/packages/veilid_support/lib/dht_support/src/dht_record/dht_record_cubit.dart similarity index 99% rename from packages/veilid_support/lib/dht_support/src/dht_record_cubit.dart rename to packages/veilid_support/lib/dht_support/src/dht_record/dht_record_cubit.dart index 176798f..2295d19 100644 --- a/packages/veilid_support/lib/dht_support/src/dht_record_cubit.dart +++ b/packages/veilid_support/lib/dht_support/src/dht_record/dht_record_cubit.dart @@ -4,7 +4,7 @@ import 'dart:typed_data'; import 'package:async_tools/async_tools.dart'; import 'package:bloc/bloc.dart'; -import '../../veilid_support.dart'; +import '../../../veilid_support.dart'; typedef InitialStateFunction = Future Function(DHTRecord); typedef StateFunction = Future Function( diff --git a/packages/veilid_support/lib/dht_support/src/dht_record_pool.dart b/packages/veilid_support/lib/dht_support/src/dht_record/dht_record_pool.dart similarity index 99% rename from packages/veilid_support/lib/dht_support/src/dht_record_pool.dart rename to packages/veilid_support/lib/dht_support/src/dht_record/dht_record_pool.dart index 80d1d43..cce18c7 100644 --- a/packages/veilid_support/lib/dht_support/src/dht_record_pool.dart +++ b/packages/veilid_support/lib/dht_support/src/dht_record/dht_record_pool.dart @@ -12,7 +12,6 @@ import '../../../../veilid_support.dart'; part 'dht_record_pool.freezed.dart'; part 'dht_record_pool.g.dart'; - part 'dht_record.dart'; const int watchBackoffMultiplier = 2; diff --git a/packages/veilid_support/lib/dht_support/src/dht_record_pool.freezed.dart b/packages/veilid_support/lib/dht_support/src/dht_record/dht_record_pool.freezed.dart similarity index 99% rename from packages/veilid_support/lib/dht_support/src/dht_record_pool.freezed.dart rename to packages/veilid_support/lib/dht_support/src/dht_record/dht_record_pool.freezed.dart index a00efd3..7419c31 100644 --- a/packages/veilid_support/lib/dht_support/src/dht_record_pool.freezed.dart +++ b/packages/veilid_support/lib/dht_support/src/dht_record/dht_record_pool.freezed.dart @@ -12,7 +12,7 @@ part of 'dht_record_pool.dart'; T _$identity(T value) => value; final _privateConstructorUsedError = UnsupportedError( - 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#custom-getters-and-methods'); + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); DHTRecordPoolAllocations _$DHTRecordPoolAllocationsFromJson( Map json) { diff --git a/packages/veilid_support/lib/dht_support/src/dht_record_pool.g.dart b/packages/veilid_support/lib/dht_support/src/dht_record/dht_record_pool.g.dart similarity index 100% rename from packages/veilid_support/lib/dht_support/src/dht_record_pool.g.dart rename to packages/veilid_support/lib/dht_support/src/dht_record/dht_record_pool.g.dart diff --git a/packages/veilid_support/lib/dht_support/src/dht_short_array.dart b/packages/veilid_support/lib/dht_support/src/dht_short_array.dart deleted file mode 100644 index 9205fce..0000000 --- a/packages/veilid_support/lib/dht_support/src/dht_short_array.dart +++ /dev/null @@ -1,904 +0,0 @@ -import 'dart:async'; -import 'dart:typed_data'; - -import 'package:mutex/mutex.dart'; -import 'package:protobuf/protobuf.dart'; - -import '../../../../veilid_support.dart'; -import '../proto/proto.dart' as proto; - -class _DHTShortArrayCache { - _DHTShortArrayCache() - : linkedRecords = List.empty(growable: true), - index = List.empty(growable: true), - free = List.empty(growable: true); - _DHTShortArrayCache.from(_DHTShortArrayCache other) - : linkedRecords = List.of(other.linkedRecords), - index = List.of(other.index), - free = List.of(other.free); - - final List linkedRecords; - final List index; - final List free; - - proto.DHTShortArray toProto() { - final head = proto.DHTShortArray(); - head.keys.addAll(linkedRecords.map((lr) => lr.key.toProto())); - head.index = head.index..addAll(index); - // Do not serialize free list, it gets recreated - return head; - } -} - -/////////////////////////////////////////////////////////////////////// - -class DHTShortArray { - //////////////////////////////////////////////////////////////// - // Constructors - - DHTShortArray._({required DHTRecord headRecord}) : _headRecord = headRecord { - late final int stride; - switch (headRecord.schema) { - case DHTSchemaDFLT(oCnt: final oCnt): - if (oCnt <= 1) { - throw StateError('Invalid DFLT schema in DHTShortArray'); - } - stride = oCnt - 1; - case DHTSchemaSMPL(oCnt: final oCnt, members: final members): - if (oCnt != 0 || members.length != 1 || members[0].mCnt <= 1) { - throw StateError('Invalid SMPL schema in DHTShortArray'); - } - stride = members[0].mCnt - 1; - } - assert(stride <= maxElements, 'stride too long'); - _stride = stride; - } - - // Create a DHTShortArray - // if smplWriter is specified, uses a SMPL schema with a single writer - // rather than the key owner - static Future create( - {int stride = maxElements, - VeilidRoutingContext? routingContext, - TypedKey? parent, - DHTRecordCrypto? crypto, - KeyPair? smplWriter}) async { - assert(stride <= maxElements, 'stride too long'); - final pool = DHTRecordPool.instance; - - late final DHTRecord dhtRecord; - if (smplWriter != null) { - final schema = DHTSchema.smpl( - oCnt: 0, - members: [DHTSchemaMember(mKey: smplWriter.key, mCnt: stride + 1)]); - dhtRecord = await pool.create( - parent: parent, - routingContext: routingContext, - schema: schema, - crypto: crypto, - writer: smplWriter); - } else { - final schema = DHTSchema.dflt(oCnt: stride + 1); - dhtRecord = await pool.create( - parent: parent, - routingContext: routingContext, - schema: schema, - crypto: crypto); - } - - try { - final dhtShortArray = DHTShortArray._(headRecord: dhtRecord); - if (!await dhtShortArray._tryWriteHead()) { - throw StateError('Failed to write head at this time'); - } - return dhtShortArray; - } on Exception catch (_) { - await dhtRecord.delete(); - rethrow; - } - } - - static Future openRead(TypedKey headRecordKey, - {VeilidRoutingContext? routingContext, - TypedKey? parent, - DHTRecordCrypto? crypto}) async { - final dhtRecord = await DHTRecordPool.instance.openRead(headRecordKey, - parent: parent, routingContext: routingContext, crypto: crypto); - try { - final dhtShortArray = DHTShortArray._(headRecord: dhtRecord); - await dhtShortArray._refreshHead(); - return dhtShortArray; - } on Exception catch (_) { - await dhtRecord.close(); - rethrow; - } - } - - static Future openWrite( - TypedKey headRecordKey, - KeyPair writer, { - VeilidRoutingContext? routingContext, - TypedKey? parent, - DHTRecordCrypto? crypto, - }) async { - final dhtRecord = await DHTRecordPool.instance.openWrite( - headRecordKey, writer, - parent: parent, routingContext: routingContext, crypto: crypto); - try { - final dhtShortArray = DHTShortArray._(headRecord: dhtRecord); - await dhtShortArray._refreshHead(); - return dhtShortArray; - } on Exception catch (_) { - await dhtRecord.close(); - rethrow; - } - } - - static Future openOwned( - OwnedDHTRecordPointer ownedDHTRecordPointer, { - required TypedKey parent, - VeilidRoutingContext? routingContext, - DHTRecordCrypto? crypto, - }) => - openWrite( - ownedDHTRecordPointer.recordKey, - ownedDHTRecordPointer.owner, - routingContext: routingContext, - parent: parent, - crypto: crypto, - ); - - //////////////////////////////////////////////////////////////////////////// - // Public API - - /// Returns the first record in the DHTShortArray which contains its control - /// information. - DHTRecord get record => _headRecord; - - /// Returns the number of elements in the DHTShortArray - int get length => _head.index.length; - - /// Free all resources for the DHTShortArray - Future close() async { - await _watchController?.close(); - final futures = >[_headRecord.close()]; - for (final lr in _head.linkedRecords) { - futures.add(lr.close()); - } - await Future.wait(futures); - } - - /// Free all resources for the DHTShortArray and delete it from the DHT - Future delete() async { - await _watchController?.close(); - - final futures = >[_headRecord.delete()]; - for (final lr in _head.linkedRecords) { - futures.add(lr.delete()); - } - await Future.wait(futures); - } - - /// Runs a closure that guarantees the DHTShortArray - /// will be closed upon exit, even if an uncaught exception is thrown - Future scope(Future Function(DHTShortArray) scopeFunction) async { - try { - return await scopeFunction(this); - } finally { - await close(); - } - } - - /// Runs a closure that guarantees the DHTShortArray - /// will be closed upon exit, and deleted if an an - /// uncaught exception is thrown - Future deleteScope( - Future Function(DHTShortArray) scopeFunction) async { - try { - final out = await scopeFunction(this); - await close(); - return out; - } on Exception catch (_) { - await delete(); - rethrow; - } - } - - /// Return the item at position 'pos' in the DHTShortArray. If 'foreRefresh' - /// is specified, the network will always be checked for newer values - /// rather than returning the existing locally stored copy of the elements. - Future getItem(int pos, {bool forceRefresh = false}) async { - await _refreshHead(forceRefresh: forceRefresh, onlyUpdates: true); - - if (pos < 0 || pos >= _head.index.length) { - throw IndexError.withLength(pos, _head.index.length); - } - final index = _head.index[pos]; - final recordNumber = index ~/ _stride; - final record = _getLinkedRecord(recordNumber); - assert(record != null, 'Record does not exist'); - - final recordSubkey = (index % _stride) + ((recordNumber == 0) ? 1 : 0); - return record!.get(subkey: recordSubkey, forceRefresh: forceRefresh); - } - - /// Return a list of all of the items in the DHTShortArray. If 'foreRefresh' - /// is specified, the network will always be checked for newer values - /// rather than returning the existing locally stored copy of the elements. - Future?> getAllItems({bool forceRefresh = false}) async { - await _refreshHead(forceRefresh: forceRefresh, onlyUpdates: true); - - final out = []; - - for (var pos = 0; pos < _head.index.length; pos++) { - final index = _head.index[pos]; - final recordNumber = index ~/ _stride; - final record = _getLinkedRecord(recordNumber); - if (record == null) { - assert(record != null, 'Record does not exist'); - return null; - } - - final recordSubkey = (index % _stride) + ((recordNumber == 0) ? 1 : 0); - final elem = - await record.get(subkey: recordSubkey, forceRefresh: forceRefresh); - if (elem == null) { - return null; - } - out.add(elem); - } - - return out; - } - - /// Convenience function: - /// Like getItem but also parses the returned element as JSON - Future getItemJson(T Function(dynamic) fromJson, int pos, - {bool forceRefresh = false}) => - getItem(pos, forceRefresh: forceRefresh) - .then((out) => jsonDecodeOptBytes(fromJson, out)); - - /// Convenience function: - /// Like getAllItems but also parses the returned elements as JSON - Future?> getAllItemsJson(T Function(dynamic) fromJson, - {bool forceRefresh = false}) => - getAllItems(forceRefresh: forceRefresh) - .then((out) => out?.map(fromJson).toList()); - - /// Convenience function: - /// Like getItem but also parses the returned element as a protobuf object - Future getItemProtobuf( - T Function(List) fromBuffer, int pos, - {bool forceRefresh = false}) => - getItem(pos, forceRefresh: forceRefresh) - .then((out) => (out == null) ? null : fromBuffer(out)); - - /// Convenience function: - /// Like getAllItems but also parses the returned elements as protobuf objects - Future?> getAllItemsProtobuf( - T Function(List) fromBuffer, - {bool forceRefresh = false}) => - getAllItems(forceRefresh: forceRefresh) - .then((out) => out?.map(fromBuffer).toList()); - - /// Try to add an item to the end of the DHTShortArray. Return true if the - /// element was successfully added, and false if the state changed before - /// the element could be added or a newer value was found on the network. - /// This may throw an exception if the number elements added exceeds the - /// built-in limit of 'maxElements = 256' entries. - Future tryAddItem(Uint8List value) async { - await _refreshHead(onlyUpdates: true); - - final oldHead = _DHTShortArrayCache.from(_head); - late final int pos; - try { - // Allocate empty index - final idx = _emptyIndex(); - // Add new index - pos = _head.index.length; - _head.index.add(idx); - - // Write new head - if (!await _tryWriteHead()) { - // Failed to write head means head got overwritten - return false; - } - } on Exception catch (_) { - // Exception on write means state needs to be reverted - _head = oldHead; - return false; - } - - // Head write succeeded, now write item - await eventualWriteItem(pos, value); - return true; - } - - /// Try to insert an item as position 'pos' of the DHTShortArray. - /// Return true if the element was successfully inserted, and false if the - /// state changed before the element could be inserted or a newer value was - /// found on the network. - /// This may throw an exception if the number elements added exceeds the - /// built-in limit of 'maxElements = 256' entries. - Future tryInsertItem(int pos, Uint8List value) async { - await _refreshHead(onlyUpdates: true); - - final oldHead = _DHTShortArrayCache.from(_head); - try { - // Allocate empty index - final idx = _emptyIndex(); - // Add new index - _head.index.insert(pos, idx); - - // Write new head - if (!await _tryWriteHead()) { - // Failed to write head means head got overwritten - return false; - } - } on Exception catch (_) { - // Exception on write means state needs to be reverted - _head = oldHead; - return false; - } - - // Head write succeeded, now write item - await eventualWriteItem(pos, value); - return true; - } - - /// Try to swap items at position 'aPos' and 'bPos' in the DHTShortArray. - /// Return true if the elements were successfully swapped, and false if the - /// state changed before the elements could be swapped or newer values were - /// found on the network. - Future trySwapItem(int aPos, int bPos) async { - if (aPos == bPos) { - return false; - } - await _refreshHead(onlyUpdates: true); - - final oldHead = _DHTShortArrayCache.from(_head); - try { - // Add new index - final aIdx = _head.index[aPos]; - final bIdx = _head.index[bPos]; - _head.index[aPos] = bIdx; - _head.index[bPos] = aIdx; - - // Write new head - if (!await _tryWriteHead()) { - // Failed to write head means head got overwritten - return false; - } - } on Exception catch (_) { - // Exception on write means state needs to be reverted - _head = oldHead; - return false; - } - - // A change happened, notify any listeners - _watchController?.sink.add(null); - return true; - } - - /// Try to remove an item at position 'pos' in the DHTShortArray. - /// Return the element if it was successfully removed, and null if the - /// state changed before the elements could be removed or newer values were - /// found on the network. - Future tryRemoveItem(int pos) async { - await _refreshHead(onlyUpdates: true); - - final oldHead = _DHTShortArrayCache.from(_head); - try { - final removedIdx = _head.index.removeAt(pos); - _freeIndex(removedIdx); - final recordNumber = removedIdx ~/ _stride; - final record = _getLinkedRecord(recordNumber); - assert(record != null, 'Record does not exist'); - final recordSubkey = - (removedIdx % _stride) + ((recordNumber == 0) ? 1 : 0); - - // Write new head - if (!await _tryWriteHead()) { - // Failed to write head means head got overwritten - return null; - } - - final result = await record!.get(subkey: recordSubkey); - - // A change happened, notify any listeners - _watchController?.sink.add(null); - - return result; - } on Exception catch (_) { - // Exception on write means state needs to be reverted - _head = oldHead; - return null; - } - } - - /// Convenience function: - /// Like removeItem but also parses the returned element as JSON - Future tryRemoveItemJson( - T Function(dynamic) fromJson, - int pos, - ) => - tryRemoveItem(pos).then((out) => jsonDecodeOptBytes(fromJson, out)); - - /// Convenience function: - /// Like removeItem but also parses the returned element as JSON - Future tryRemoveItemProtobuf( - T Function(List) fromBuffer, int pos) => - getItem(pos).then((out) => (out == null) ? null : fromBuffer(out)); - - /// Try to remove all items in the DHTShortArray. - /// Return true if it was successfully cleared, and false if the - /// state changed before the elements could be cleared or newer values were - /// found on the network. - Future tryClear() async { - await _refreshHead(onlyUpdates: true); - - final oldHead = _DHTShortArrayCache.from(_head); - try { - _head.index.clear(); - _head.free.clear(); - - // Write new head - if (!await _tryWriteHead()) { - // Failed to write head means head got overwritten - return false; - } - } on Exception catch (_) { - // Exception on write means state needs to be reverted - _head = oldHead; - return false; - } - - // A change happened, notify any listeners - _watchController?.sink.add(null); - - return true; - } - - /// Try to set an item at position 'pos' of the DHTShortArray. - /// Return true if the element was successfully set, and false if the - /// state changed before the element could be set or a newer value was - /// found on the network. - /// This may throw an exception if the position elements the built-in limit of - /// 'maxElements = 256' entries. - Future tryWriteItem(int pos, Uint8List newValue) async { - if (await _refreshHead(onlyUpdates: true)) { - throw StateError('structure changed'); - } - if (pos < 0 || pos >= _head.index.length) { - throw IndexError.withLength(pos, _head.index.length); - } - final index = _head.index[pos]; - - final recordNumber = index ~/ _stride; - final record = await _getOrCreateLinkedRecord(recordNumber); - - final recordSubkey = (index % _stride) + ((recordNumber == 0) ? 1 : 0); - final result = await record.tryWriteBytes(newValue, subkey: recordSubkey); - - if (result == null) { - // A newer value was not found, so the change took - _watchController?.sink.add(null); - } - return result; - } - - /// Set an item at position 'pos' of the DHTShortArray. Retries until the - /// value being written is successfully made the newest value of the element. - /// This may throw an exception if the position elements the built-in limit of - /// 'maxElements = 256' entries. - Future eventualWriteItem(int pos, Uint8List newValue) async { - Uint8List? oldData; - do { - // Set it back - oldData = await tryWriteItem(pos, newValue); - - // Repeat if newer data on the network was found - } while (oldData != null); - } - - /// Change an item at position 'pos' of the DHTShortArray. - /// Runs with the value of the old element at that position such that it can - /// be changed to the returned value from tha closure. Retries until the - /// value being written is successfully made the newest value of the element. - /// This may throw an exception if the position elements the built-in limit of - /// 'maxElements = 256' entries. - Future eventualUpdateItem( - int pos, Future Function(Uint8List? oldValue) update) async { - var oldData = await getItem(pos); - // Ensure it exists already - if (oldData == null) { - throw const FormatException('value does not exist'); - } - do { - // Update the data - final updatedData = await update(oldData); - - // Set it back - oldData = await tryWriteItem(pos, updatedData); - - // Repeat if newer data on the network was found - } while (oldData != null); - } - - /// Convenience function: - /// Like tryWriteItem but also encodes the input value as JSON and parses the - /// returned element as JSON - Future tryWriteItemJson( - T Function(dynamic) fromJson, - int pos, - T newValue, - ) => - tryWriteItem(pos, jsonEncodeBytes(newValue)) - .then((out) => jsonDecodeOptBytes(fromJson, out)); - - /// Convenience function: - /// Like tryWriteItem but also encodes the input value as a protobuf object - /// and parses the returned element as a protobuf object - Future tryWriteItemProtobuf( - T Function(List) fromBuffer, - int pos, - T newValue, - ) => - tryWriteItem(pos, newValue.writeToBuffer()).then((out) { - if (out == null) { - return null; - } - return fromBuffer(out); - }); - - /// Convenience function: - /// Like eventualWriteItem but also encodes the input value as JSON and parses - /// the returned element as JSON - Future eventualWriteItemJson(int pos, T newValue) => - eventualWriteItem(pos, jsonEncodeBytes(newValue)); - - /// Convenience function: - /// Like eventualWriteItem but also encodes the input value as a protobuf - /// object and parses the returned element as a protobuf object - Future eventualWriteItemProtobuf( - int pos, T newValue, - {int subkey = -1}) => - eventualWriteItem(pos, newValue.writeToBuffer()); - - /// Convenience function: - /// Like eventualUpdateItem but also encodes the input value as JSON - Future eventualUpdateItemJson( - T Function(dynamic) fromJson, - int pos, - Future Function(T?) update, - ) => - eventualUpdateItem(pos, jsonUpdate(fromJson, update)); - - /// Convenience function: - /// Like eventualUpdateItem but also encodes the input value as a protobuf - /// object - Future eventualUpdateItemProtobuf( - T Function(List) fromBuffer, - int pos, - Future Function(T?) update, - ) => - eventualUpdateItem(pos, protobufUpdate(fromBuffer, update)); - - //////////////////////////////////////////////////////////////// - // Internal Operations - - DHTRecord? _getLinkedRecord(int recordNumber) { - if (recordNumber == 0) { - return _headRecord; - } - recordNumber--; - if (recordNumber >= _head.linkedRecords.length) { - return null; - } - return _head.linkedRecords[recordNumber]; - } - - Future _getOrCreateLinkedRecord(int recordNumber) async { - if (recordNumber == 0) { - return _headRecord; - } - final pool = DHTRecordPool.instance; - recordNumber--; - while (recordNumber >= _head.linkedRecords.length) { - // Linked records must use SMPL schema so writer can be specified - // Use the same writer as the head record - final smplWriter = _headRecord.writer!; - final parent = pool.getParentRecordKey(_headRecord.key); - final routingContext = _headRecord.routingContext; - final crypto = _headRecord.crypto; - - final schema = DHTSchema.smpl( - oCnt: 0, - members: [DHTSchemaMember(mKey: smplWriter.key, mCnt: _stride)]); - final dhtCreateRecord = await pool.create( - parent: parent, - routingContext: routingContext, - schema: schema, - crypto: crypto, - writer: smplWriter); - // Reopen with SMPL writer - await dhtCreateRecord.close(); - final dhtRecord = await pool.openWrite(dhtCreateRecord.key, smplWriter, - parent: parent, routingContext: routingContext, crypto: crypto); - - // Add to linked records - _head.linkedRecords.add(dhtRecord); - if (!await _tryWriteHead()) { - await _refreshHead(); - } - } - return _head.linkedRecords[recordNumber]; - } - - int _emptyIndex() { - if (_head.free.isNotEmpty) { - return _head.free.removeLast(); - } - if (_head.index.length == maxElements) { - throw StateError('too many elements'); - } - return _head.index.length; - } - - void _freeIndex(int idx) { - _head.free.add(idx); - // xxx: free list optimization here? - } - - /// Serialize and write out the current head record, possibly updating it - /// if a newer copy is available online. Returns true if the write was - /// successful - Future _tryWriteHead() async { - final head = _head.toProto(); - final headBuffer = head.writeToBuffer(); - - final existingData = await _headRecord.tryWriteBytes(headBuffer); - if (existingData != null) { - // Head write failed, incorporate update - await _newHead(proto.DHTShortArray.fromBuffer(existingData)); - return false; - } - - return true; - } - - /// Validate the head from the DHT is properly formatted - /// and calculate the free list from it while we're here - List _validateHeadCacheData( - List> linkedKeys, List index) { - // Ensure nothing is duplicated in the linked keys set - final newKeys = linkedKeys.toSet(); - assert(newKeys.length <= (maxElements + (_stride - 1)) ~/ _stride, - 'too many keys'); - assert(newKeys.length == linkedKeys.length, 'duplicated linked keys'); - final newIndex = index.toSet(); - assert(newIndex.length <= maxElements, 'too many indexes'); - assert(newIndex.length == index.length, 'duplicated index locations'); - // Ensure all the index keys fit into the existing records - final indexCapacity = (linkedKeys.length + 1) * _stride; - int? maxIndex; - for (final idx in newIndex) { - assert(idx >= 0 || idx < indexCapacity, 'index out of range'); - if (maxIndex == null || idx > maxIndex) { - maxIndex = idx; - } - } - final free = []; - if (maxIndex != null) { - for (var i = 0; i < maxIndex; i++) { - if (!newIndex.contains(i)) { - free.add(i); - } - } - } - return free; - } - - /// Open a linked record for reading or writing, same as the head record - Future _openLinkedRecord(TypedKey recordKey) async { - final writer = _headRecord.writer; - return (writer != null) - ? await DHTRecordPool.instance.openWrite( - recordKey, - writer, - parent: _headRecord.key, - routingContext: _headRecord.routingContext, - ) - : await DHTRecordPool.instance.openRead( - recordKey, - parent: _headRecord.key, - routingContext: _headRecord.routingContext, - ); - } - - /// Validate a new head record - Future _newHead(proto.DHTShortArray head) async { - // Get the set of new linked keys and validate it - final linkedKeys = head.keys.map((p) => p.toVeilid()).toList(); - final index = head.index; - final free = _validateHeadCacheData(linkedKeys, index); - - // See which records are actually new - final oldRecords = Map.fromEntries( - _head.linkedRecords.map((lr) => MapEntry(lr.key, lr))); - final newRecords = {}; - final sameRecords = {}; - try { - for (var n = 0; n < linkedKeys.length; n++) { - final newKey = linkedKeys[n]; - final oldRecord = oldRecords[newKey]; - if (oldRecord == null) { - // Open the new record - final newRecord = await _openLinkedRecord(newKey); - newRecords[newKey] = newRecord; - } else { - sameRecords[newKey] = oldRecord; - } - } - } on Exception catch (_) { - // On any exception close the records we have opened - await Future.wait(newRecords.entries.map((e) => e.value.close())); - rethrow; - } - - // From this point forward we should not throw an exception or everything - // is possibly invalid. Just pass the exception up it happens and the caller - // will have to delete this short array and reopen it if it can - await Future.wait(oldRecords.entries - .where((e) => !sameRecords.containsKey(e.key)) - .map((e) => e.value.close())); - - // Figure out which indices are free - - // Make the new head cache - _head = _DHTShortArrayCache() - ..linkedRecords.addAll( - linkedKeys.map((key) => (sameRecords[key] ?? newRecords[key])!)) - ..index.addAll(index) - ..free.addAll(free); - - // Update watch if we have one in case linked records have been added - if (_watchController != null) { - await _watchAllRecords(); - } - } - - /// Pull the latest or updated copy of the head record from the network - Future _refreshHead( - {bool forceRefresh = true, bool onlyUpdates = false}) async { - // Get an updated head record copy if one exists - final head = await _headRecord.getProtobuf(proto.DHTShortArray.fromBuffer, - subkey: 0, forceRefresh: forceRefresh, onlyUpdates: onlyUpdates); - if (head == null) { - if (onlyUpdates) { - // No update - return false; - } - throw StateError('head missing during refresh'); - } - - await _newHead(head); - - return true; - } - - // Watch head and all linked records - Future _watchAllRecords() async { - // This will update any existing watches if necessary - try { - await [_headRecord.watch(), ..._head.linkedRecords.map((r) => r.watch())] - .wait; - - // Update changes to the head record - // Don't watch for local changes because this class already handles - // notifying listeners and knows when it makes local changes - if (!_subscriptions.containsKey(_headRecord.key)) { - _subscriptions[_headRecord.key] = - await _headRecord.listen(localChanges: false, _onUpdateRecord); - } - // Update changes to any linked records - for (final lr in _head.linkedRecords) { - if (!_subscriptions.containsKey(lr.key)) { - _subscriptions[lr.key] = - await lr.listen(localChanges: false, _onUpdateRecord); - } - } - } on Exception { - // If anything fails, try to cancel the watches - await _cancelRecordWatches(); - rethrow; - } - } - - // Stop watching for changes to head and linked records - Future _cancelRecordWatches() async { - await _headRecord.cancelWatch(); - for (final lr in _head.linkedRecords) { - await lr.cancelWatch(); - } - await _subscriptions.values.map((s) => s.cancel()).wait; - _subscriptions.clear(); - } - - // Called when a head or linked record changes - Future _onUpdateRecord( - DHTRecord record, Uint8List? data, List subkeys) async { - // If head record subkey zero changes, then the layout - // of the dhtshortarray has changed - var updateHead = false; - if (record == _headRecord && subkeys.containsSubkey(0)) { - updateHead = true; - } - - // If we have any other subkeys to update, do them first - final unord = List>.empty(growable: true); - for (final skr in subkeys) { - for (var subkey = skr.low; subkey <= skr.high; subkey++) { - // Skip head subkey - if (updateHead && subkey == 0) { - continue; - } - // Get the subkey, which caches the result in the local record store - unord.add(record.get(subkey: subkey, forceRefresh: true)); - } - } - await unord.wait; - - // Then update the head record - if (updateHead) { - await _refreshHead(forceRefresh: false); - } - // Then commit the change to any listeners - _watchController?.sink.add(null); - } - - Future> listen( - void Function() onChanged, - ) => - _listenMutex.protect(() async { - // If don't have a controller yet, set it up - if (_watchController == null) { - // Set up watch requirements - _watchController = StreamController.broadcast(onCancel: () { - // If there are no more listeners then we can get - // rid of the controller and drop our subscriptions - unawaited(_listenMutex.protect(() async { - // Cancel watches of head and linked records - await _cancelRecordWatches(); - _watchController = null; - })); - }); - - // Start watching head and linked records - await _watchAllRecords(); - } - // Return subscription - return _watchController!.stream.listen((_) => onChanged()); - }); - - //////////////////////////////////////////////////////////////// - // Fields - - static const maxElements = 256; - - // Head DHT record - final DHTRecord _headRecord; - // How many elements per linked record - late final int _stride; - // Cached representation refreshed from head record - _DHTShortArrayCache _head = _DHTShortArrayCache(); - // Subscription to head and linked record internal changes - final Map> _subscriptions = - {}; - // Stream of external changes - StreamController? _watchController; - // Watch mutex to ensure we keep the representation valid - final Mutex _listenMutex = Mutex(); - // Head/element mutex to ensure we keep the representation valid - final Mutex _headMutex = Mutex(); -} diff --git a/packages/veilid_support/lib/dht_support/src/dht_short_array/barrel.dart b/packages/veilid_support/lib/dht_support/src/dht_short_array/barrel.dart new file mode 100644 index 0000000..6dc1c97 --- /dev/null +++ b/packages/veilid_support/lib/dht_support/src/dht_short_array/barrel.dart @@ -0,0 +1,2 @@ +export 'dht_short_array.dart'; +export 'dht_short_array_cubit.dart'; diff --git a/packages/veilid_support/lib/dht_support/src/dht_short_array/dht_short_array.dart b/packages/veilid_support/lib/dht_support/src/dht_short_array/dht_short_array.dart new file mode 100644 index 0000000..8136a61 --- /dev/null +++ b/packages/veilid_support/lib/dht_support/src/dht_short_array/dht_short_array.dart @@ -0,0 +1,596 @@ +import 'dart:async'; +import 'dart:typed_data'; + +import 'package:mutex/mutex.dart'; +import 'package:protobuf/protobuf.dart'; + +import '../../../veilid_support.dart'; +import '../../proto/proto.dart' as proto; + +part 'dht_short_array_head.dart'; + +/////////////////////////////////////////////////////////////////////// + +class DHTShortArray { + //////////////////////////////////////////////////////////////// + // Constructors + + DHTShortArray._({required DHTRecord headRecord}) + : _head = _DHTShortArrayHead(headRecord: headRecord) {} + + // Create a DHTShortArray + // if smplWriter is specified, uses a SMPL schema with a single writer + // rather than the key owner + static Future create( + {int stride = maxElements, + VeilidRoutingContext? routingContext, + TypedKey? parent, + DHTRecordCrypto? crypto, + KeyPair? smplWriter}) async { + assert(stride <= maxElements, 'stride too long'); + final pool = DHTRecordPool.instance; + + late final DHTRecord dhtRecord; + if (smplWriter != null) { + final schema = DHTSchema.smpl( + oCnt: 0, + members: [DHTSchemaMember(mKey: smplWriter.key, mCnt: stride + 1)]); + dhtRecord = await pool.create( + parent: parent, + routingContext: routingContext, + schema: schema, + crypto: crypto, + writer: smplWriter); + } else { + final schema = DHTSchema.dflt(oCnt: stride + 1); + dhtRecord = await pool.create( + parent: parent, + routingContext: routingContext, + schema: schema, + crypto: crypto); + } + + try { + final dhtShortArray = DHTShortArray._(headRecord: dhtRecord); + if (!await dhtShortArray._head._tryWriteHead()) { + throw StateError('Failed to write head at this time'); + } + return dhtShortArray; + } on Exception catch (_) { + await dhtRecord.delete(); + rethrow; + } + } + + static Future openRead(TypedKey headRecordKey, + {VeilidRoutingContext? routingContext, + TypedKey? parent, + DHTRecordCrypto? crypto}) async { + final dhtRecord = await DHTRecordPool.instance.openRead(headRecordKey, + parent: parent, routingContext: routingContext, crypto: crypto); + try { + final dhtShortArray = DHTShortArray._(headRecord: dhtRecord); + await dhtShortArray._head._refreshInner(); + return dhtShortArray; + } on Exception catch (_) { + await dhtRecord.close(); + rethrow; + } + } + + static Future openWrite( + TypedKey headRecordKey, + KeyPair writer, { + VeilidRoutingContext? routingContext, + TypedKey? parent, + DHTRecordCrypto? crypto, + }) async { + final dhtRecord = await DHTRecordPool.instance.openWrite( + headRecordKey, writer, + parent: parent, routingContext: routingContext, crypto: crypto); + try { + final dhtShortArray = DHTShortArray._(headRecord: dhtRecord); + await dhtShortArray._head._refreshInner(); + return dhtShortArray; + } on Exception catch (_) { + await dhtRecord.close(); + rethrow; + } + } + + static Future openOwned( + OwnedDHTRecordPointer ownedDHTRecordPointer, { + required TypedKey parent, + VeilidRoutingContext? routingContext, + DHTRecordCrypto? crypto, + }) => + openWrite( + ownedDHTRecordPointer.recordKey, + ownedDHTRecordPointer.owner, + routingContext: routingContext, + parent: parent, + crypto: crypto, + ); + + //////////////////////////////////////////////////////////////////////////// + // Public API + + // External references for the shortarray + TypedKey get recordKey => _head.headRecord.key; + OwnedDHTRecordPointer get recordPointer => + _head.headRecord.ownedDHTRecordPointer; + + /// Returns the number of elements in the DHTShortArray + int get length => _head.index.length; + + /// Free all resources for the DHTShortArray + Future close() async { + await _watchController?.close(); + await _head.close(); + } + + /// Free all resources for the DHTShortArray and delete it from the DHT + Future delete() async { + await _watchController?.close(); + await _head.delete(); + } + + /// Runs a closure that guarantees the DHTShortArray + /// will be closed upon exit, even if an uncaught exception is thrown + Future scope(Future Function(DHTShortArray) scopeFunction) async { + try { + return await scopeFunction(this); + } finally { + await close(); + } + } + + /// Runs a closure that guarantees the DHTShortArray + /// will be closed upon exit, and deleted if an an + /// uncaught exception is thrown + Future deleteScope( + Future Function(DHTShortArray) scopeFunction) async { + try { + final out = await scopeFunction(this); + await close(); + return out; + } on Exception catch (_) { + await delete(); + rethrow; + } + } + + /// Return the item at position 'pos' in the DHTShortArray. If 'forceRefresh' + /// is specified, the network will always be checked for newer values + /// rather than returning the existing locally stored copy of the elements. + Future getItem(int pos, {bool forceRefresh = false}) async => + _head.operate( + (head) async => _getItemInner(head, pos, forceRefresh: forceRefresh)); + + Future _getItemInner(_DHTShortArrayHead head, int pos, + {bool forceRefresh = false}) async { + if (pos < 0 || pos >= head.index.length) { + throw IndexError.withLength(pos, head.index.length); + } + + final index = head.index[pos]; + final recordNumber = index ~/ head.stride; + final record = head.getLinkedRecord(recordNumber); + if (record == null) { + throw StateError('Record does not exist'); + } + final recordSubkey = (index % head.stride) + ((recordNumber == 0) ? 1 : 0); + + final refresh = forceRefresh || head.indexNeedsRefresh(index); + + final out = record.get(subkey: recordSubkey, forceRefresh: refresh); + + await head.updateIndexSeq(index, false); + + return out; + } + + /// Return a list of all of the items in the DHTShortArray. If 'forceRefresh' + /// is specified, the network will always be checked for newer values + /// rather than returning the existing locally stored copy of the elements. + Future?> getAllItems({bool forceRefresh = false}) async => + _head.operate((head) async { + final out = []; + + for (var pos = 0; pos < head.index.length; pos++) { + final elem = + await _getItemInner(head, pos, forceRefresh: forceRefresh); + if (elem == null) { + return null; + } + out.add(elem); + } + + return out; + }); + + /// Convenience function: + /// Like getItem but also parses the returned element as JSON + Future getItemJson(T Function(dynamic) fromJson, int pos, + {bool forceRefresh = false}) => + getItem(pos, forceRefresh: forceRefresh) + .then((out) => jsonDecodeOptBytes(fromJson, out)); + + /// Convenience function: + /// Like getAllItems but also parses the returned elements as JSON + Future?> getAllItemsJson(T Function(dynamic) fromJson, + {bool forceRefresh = false}) => + getAllItems(forceRefresh: forceRefresh) + .then((out) => out?.map(fromJson).toList()); + + /// Convenience function: + /// Like getItem but also parses the returned element as a protobuf object + Future getItemProtobuf( + T Function(List) fromBuffer, int pos, + {bool forceRefresh = false}) => + getItem(pos, forceRefresh: forceRefresh) + .then((out) => (out == null) ? null : fromBuffer(out)); + + /// Convenience function: + /// Like getAllItems but also parses the returned elements as protobuf objects + Future?> getAllItemsProtobuf( + T Function(List) fromBuffer, + {bool forceRefresh = false}) => + getAllItems(forceRefresh: forceRefresh) + .then((out) => out?.map(fromBuffer).toList()); + + /// Try to add an item to the end of the DHTShortArray. Return true if the + /// element was successfully added, and false if the state changed before + /// the element could be added or a newer value was found on the network. + /// This may throw an exception if the number elements added exceeds the + /// built-in limit of 'maxElements = 256' entries. + Future tryAddItem(Uint8List value) async { + final out = await _head + .operateWrite((head) async => _tryAddItemInner(head, value)) ?? + false; + + // Send update + _watchController?.sink.add(null); + + return out; + } + + Future _tryAddItemInner( + _DHTShortArrayHead head, Uint8List value) async { + // Allocate empty index + final index = head.emptyIndex(); + + // Add new index + final pos = head.index.length; + head.index.add(index); + + // Write item + final (_, wasSet) = await _tryWriteItemInner(head, pos, value); + if (!wasSet) { + return false; + } + + // Get sequence number written + await head.updateIndexSeq(index, true); + + return true; + } + + /// Try to insert an item as position 'pos' of the DHTShortArray. + /// Return true if the element was successfully inserted, and false if the + /// state changed before the element could be inserted or a newer value was + /// found on the network. + /// This may throw an exception if the number elements added exceeds the + /// built-in limit of 'maxElements = 256' entries. + Future tryInsertItem(int pos, Uint8List value) async { + final out = await _head.operateWrite( + (head) async => _tryInsertItemInner(head, pos, value)) ?? + false; + + // Send update + _watchController?.sink.add(null); + + return out; + } + + Future _tryInsertItemInner( + _DHTShortArrayHead head, int pos, Uint8List value) async { + // Allocate empty index + final index = head.emptyIndex(); + + // Add new index + _head.index.insert(pos, index); + + // Write item + final (_, wasSet) = await _tryWriteItemInner(head, pos, value); + if (!wasSet) { + return false; + } + + // Get sequence number written + await head.updateIndexSeq(index, true); + + return true; + } + + /// Try to swap items at position 'aPos' and 'bPos' in the DHTShortArray. + /// Return true if the elements were successfully swapped, and false if the + /// state changed before the elements could be swapped or newer values were + /// found on the network. + /// This may throw an exception if either of the positions swapped exceed + /// the length of the list + + Future trySwapItem(int aPos, int bPos) async { + final out = await _head.operateWrite( + (head) async => _trySwapItemInner(head, aPos, bPos)) ?? + false; + + // Send update + _watchController?.sink.add(null); + + return out; + } + + Future _trySwapItemInner( + _DHTShortArrayHead head, int aPos, int bPos) async { + // No-op case + if (aPos == bPos) { + return true; + } + + // Swap indices + final aIdx = _head.index[aPos]; + final bIdx = _head.index[bPos]; + _head.index[aPos] = bIdx; + _head.index[bPos] = aIdx; + + return true; + } + + /// Try to remove an item at position 'pos' in the DHTShortArray. + /// Return the element if it was successfully removed, and null if the + /// state changed before the elements could be removed or newer values were + /// found on the network. + /// This may throw an exception if the position removed exceeeds the length of + /// the list. + + Future tryRemoveItem(int pos) async { + final out = + _head.operateWrite((head) async => _tryRemoveItemInner(head, pos)); + + // Send update + _watchController?.sink.add(null); + + return out; + } + + Future _tryRemoveItemInner( + _DHTShortArrayHead head, int pos) async { + final index = _head.index.removeAt(pos); + final recordNumber = index ~/ head.stride; + final record = head.getLinkedRecord(recordNumber); + if (record == null) { + throw StateError('Record does not exist'); + } + final recordSubkey = (index % head.stride) + ((recordNumber == 0) ? 1 : 0); + + final result = await record.get(subkey: recordSubkey); + if (result == null) { + throw StateError('Element does not exist'); + } + + head.freeIndex(index); + return result; + } + + /// Convenience function: + /// Like removeItem but also parses the returned element as JSON + Future tryRemoveItemJson( + T Function(dynamic) fromJson, + int pos, + ) => + tryRemoveItem(pos).then((out) => jsonDecodeOptBytes(fromJson, out)); + + /// Convenience function: + /// Like removeItem but also parses the returned element as JSON + Future tryRemoveItemProtobuf( + T Function(List) fromBuffer, int pos) => + getItem(pos).then((out) => (out == null) ? null : fromBuffer(out)); + + /// Try to remove all items in the DHTShortArray. + /// Return true if it was successfully cleared, and false if the + /// state changed before the elements could be cleared or newer values were + /// found on the network. + Future tryClear() async { + final out = + await _head.operateWrite((head) async => _tryClearInner(head)) ?? false; + + // Send update + _watchController?.sink.add(null); + + return out; + } + + Future _tryClearInner(_DHTShortArrayHead head) async { + head.index.clear(); + head.free.clear(); + return true; + } + + /// Try to set an item at position 'pos' of the DHTShortArray. + /// If the set was successful this returns: + /// * The prior contents of the element, or null if there was no value yet + /// * A boolean true + /// If the set was found a newer value on the network: + /// * The newer value of the element, or null if the head record + /// changed. + /// * A boolean false + /// This may throw an exception if the position exceeds the built-in limit of + /// 'maxElements = 256' entries. + Future<(Uint8List?, bool)> tryWriteItem(int pos, Uint8List newValue) async { + final out = await _head + .operateWrite((head) async => _tryWriteItemInner(head, pos, newValue)); + if (out == null) { + return (null, false); + } + + // Send update + _watchController?.sink.add(null); + + return out; + } + + Future<(Uint8List?, bool)> _tryWriteItemInner( + _DHTShortArrayHead head, int pos, Uint8List newValue) async { + if (pos < 0 || pos >= head.index.length) { + throw IndexError.withLength(pos, _head.index.length); + } + + final index = head.index[pos]; + final recordNumber = index ~/ head.stride; + final record = await head.getOrCreateLinkedRecord(recordNumber); + final recordSubkey = (index % head.stride) + ((recordNumber == 0) ? 1 : 0); + + final oldValue = await record.get(subkey: recordSubkey); + final result = await record.tryWriteBytes(newValue, subkey: recordSubkey); + if (result != null) { + // A result coming back means the element was overwritten already + return (result, false); + } + return (oldValue, true); + } + + /// Set an item at position 'pos' of the DHTShortArray. Retries until the + /// value being written is successfully made the newest value of the element. + /// This may throw an exception if the position elements the built-in limit of + /// 'maxElements = 256' entries. + Future eventualWriteItem(int pos, Uint8List newValue, + {Duration? timeout}) async { + await _head.operateWriteEventual((head) async { + bool wasSet; + (_, wasSet) = await _tryWriteItemInner(head, pos, newValue); + return wasSet; + }, timeout: timeout); + + // Send update + _watchController?.sink.add(null); + } + + /// Change an item at position 'pos' of the DHTShortArray. + /// Runs with the value of the old element at that position such that it can + /// be changed to the returned value from tha closure. Retries until the + /// value being written is successfully made the newest value of the element. + /// This may throw an exception if the position elements the built-in limit of + /// 'maxElements = 256' entries. + + Future eventualUpdateItem( + int pos, Future Function(Uint8List? oldValue) update, + {Duration? timeout}) async { + await _head.operateWriteEventual((head) async { + final oldData = await getItem(pos); + + // Update the data + final updatedData = await update(oldData); + + // Set it back + bool wasSet; + (_, wasSet) = await _tryWriteItemInner(head, pos, updatedData); + return wasSet; + }, timeout: timeout); + + // Send update + _watchController?.sink.add(null); + } + + /// Convenience function: + /// Like tryWriteItem but also encodes the input value as JSON and parses the + /// returned element as JSON + Future<(T?, bool)> tryWriteItemJson( + T Function(dynamic) fromJson, + int pos, + T newValue, + ) => + tryWriteItem(pos, jsonEncodeBytes(newValue)) + .then((out) => (jsonDecodeOptBytes(fromJson, out.$1), out.$2)); + + /// Convenience function: + /// Like tryWriteItem but also encodes the input value as a protobuf object + /// and parses the returned element as a protobuf object + Future<(T?, bool)> tryWriteItemProtobuf( + T Function(List) fromBuffer, + int pos, + T newValue, + ) => + tryWriteItem(pos, newValue.writeToBuffer()).then( + (out) => ((out.$1 == null ? null : fromBuffer(out.$1!)), out.$2)); + + /// Convenience function: + /// Like eventualWriteItem but also encodes the input value as JSON and parses + /// the returned element as JSON + Future eventualWriteItemJson(int pos, T newValue, + {Duration? timeout}) => + eventualWriteItem(pos, jsonEncodeBytes(newValue), timeout: timeout); + + /// Convenience function: + /// Like eventualWriteItem but also encodes the input value as a protobuf + /// object and parses the returned element as a protobuf object + Future eventualWriteItemProtobuf( + int pos, T newValue, + {int subkey = -1, Duration? timeout}) => + eventualWriteItem(pos, newValue.writeToBuffer(), timeout: timeout); + + /// Convenience function: + /// Like eventualUpdateItem but also encodes the input value as JSON + Future eventualUpdateItemJson( + T Function(dynamic) fromJson, int pos, Future Function(T?) update, + {Duration? timeout}) => + eventualUpdateItem(pos, jsonUpdate(fromJson, update), timeout: timeout); + + /// Convenience function: + /// Like eventualUpdateItem but also encodes the input value as a protobuf + /// object + Future eventualUpdateItemProtobuf( + T Function(List) fromBuffer, + int pos, + Future Function(T?) update, + {Duration? timeout}) => + eventualUpdateItem(pos, protobufUpdate(fromBuffer, update), + timeout: timeout); + + Future> listen( + void Function() onChanged, + ) => + _listenMutex.protect(() async { + // If don't have a controller yet, set it up + if (_watchController == null) { + // Set up watch requirements + _watchController = StreamController.broadcast(onCancel: () { + // If there are no more listeners then we can get + // rid of the controller and drop our subscriptions + unawaited(_listenMutex.protect(() async { + // Cancel watches of head record + await _head._cancelWatch(); + _watchController = null; + })); + }); + + // Start watching head record + await _head._watch(); + } + // Return subscription + return _watchController!.stream.listen((_) => onChanged()); + }); + + //////////////////////////////////////////////////////////////// + // Fields + + static const maxElements = 256; + + // Internal representation refreshed from head record + final _DHTShortArrayHead _head; + + // Watch mutex to ensure we keep the representation valid + final Mutex _listenMutex = Mutex(); + // Stream of external changes + StreamController? _watchController; +} diff --git a/packages/veilid_support/lib/dht_support/src/dht_short_array_cubit.dart b/packages/veilid_support/lib/dht_support/src/dht_short_array/dht_short_array_cubit.dart similarity index 97% rename from packages/veilid_support/lib/dht_support/src/dht_short_array_cubit.dart rename to packages/veilid_support/lib/dht_support/src/dht_short_array/dht_short_array_cubit.dart index 8525721..5eea23a 100644 --- a/packages/veilid_support/lib/dht_support/src/dht_short_array_cubit.dart +++ b/packages/veilid_support/lib/dht_support/src/dht_short_array/dht_short_array_cubit.dart @@ -6,7 +6,7 @@ import 'package:bloc_tools/bloc_tools.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:mutex/mutex.dart'; -import '../../veilid_support.dart'; +import '../../../veilid_support.dart'; typedef DHTShortArrayState = AsyncValue>; typedef DHTShortArrayBusyState = BlocBusyState>; @@ -29,7 +29,7 @@ class DHTShortArrayCubit extends Cubit> }); } - DHTShortArrayCubit.value({ + DHTShortArrayCubit.value({ required DHTShortArray shortArray, required T Function(List data) decodeElement, }) : _shortArray = shortArray, diff --git a/packages/veilid_support/lib/dht_support/src/dht_short_array/dht_short_array_head.dart b/packages/veilid_support/lib/dht_support/src/dht_short_array/dht_short_array_head.dart new file mode 100644 index 0000000..94aac2c --- /dev/null +++ b/packages/veilid_support/lib/dht_support/src/dht_short_array/dht_short_array_head.dart @@ -0,0 +1,471 @@ +part of 'dht_short_array.dart'; + +//////////////////////////////////////////////////////////////// +// Internal Operations + +class _DHTShortArrayHead { + _DHTShortArrayHead({required this.headRecord}) + : linkedRecords = [], + index = [], + free = [], + seqs = [], + localSeqs = [] { + _calculateStride(); + } + + proto.DHTShortArray toProto() { + final head = proto.DHTShortArray(); + head.keys.addAll(linkedRecords.map((lr) => lr.key.toProto())); + head.index.addAll(index); + head.seqs.addAll(seqs); + // Do not serialize free list, it gets recreated + // Do not serialize local seqs, they are only locally relevant + return head; + } + + Future close() async { + final futures = >[headRecord.close()]; + for (final lr in linkedRecords) { + futures.add(lr.close()); + } + await Future.wait(futures); + } + + Future delete() async { + final futures = >[headRecord.delete()]; + for (final lr in linkedRecords) { + futures.add(lr.delete()); + } + await Future.wait(futures); + } + + Future operate(Future Function(_DHTShortArrayHead) closure) async => + // ignore: prefer_expression_function_bodies + _headMutex.protect(() async { + return closure(this); + }); + + Future operateWrite( + Future Function(_DHTShortArrayHead) closure) async { + final oldLinkedRecords = List.of(linkedRecords); + final oldIndex = List.of(index); + final oldFree = List.of(free); + final oldSeqs = List.of(seqs); + try { + final out = await _headMutex.protect(() async { + final out = await closure(this); + // Write head assuming it has been changed + if (!await _tryWriteHead()) { + // Failed to write head means head got overwritten so write should + // be considered failed + return null; + } + return out; + }); + return out; + } on Exception { + // Exception means state needs to be reverted + linkedRecords = oldLinkedRecords; + index = oldIndex; + free = oldFree; + seqs = oldSeqs; + + rethrow; + } + } + + Future operateWriteEventual( + Future Function(_DHTShortArrayHead) closure, + {Duration? timeout}) async { + late List oldLinkedRecords; + late List oldIndex; + late List oldFree; + late List oldSeqs; + + final timeoutTs = timeout == null + ? null + : Veilid.instance.now().offset(TimestampDuration.fromDuration(timeout)); + try { + await _headMutex.protect(() async { + // Iterate until we have a successful element and head write + do { + // Save off old values each pass of tryWriteHead because the head + // will have changed + oldLinkedRecords = List.of(linkedRecords); + oldIndex = List.of(index); + oldFree = List.of(free); + oldSeqs = List.of(seqs); + + // Try to do the element write + do { + if (timeoutTs != null) { + final now = Veilid.instance.now(); + if (now >= timeoutTs) { + throw TimeoutException('timeout reached'); + } + } + } while (!await closure(this)); + + // Try to do the head write + } while (!await _tryWriteHead()); + }); + } on Exception { + // Exception means state needs to be reverted + linkedRecords = oldLinkedRecords; + index = oldIndex; + free = oldFree; + seqs = oldSeqs; + + rethrow; + } + } + + /// Serialize and write out the current head record, possibly updating it + /// if a newer copy is available online. Returns true if the write was + /// successful + Future _tryWriteHead() async { + final headBuffer = toProto().writeToBuffer(); + + final existingData = await headRecord.tryWriteBytes(headBuffer); + if (existingData != null) { + // Head write failed, incorporate update + await _updateHead(proto.DHTShortArray.fromBuffer(existingData)); + return false; + } + + return true; + } + + /// Validate a new head record that has come in from the network + Future _updateHead(proto.DHTShortArray head) async { + // Get the set of new linked keys and validate it + final updatedLinkedKeys = head.keys.map((p) => p.toVeilid()).toList(); + final updatedIndex = List.of(head.index); + final updatedSeqs = List.of(head.seqs); + final updatedFree = _makeFreeList(updatedLinkedKeys, updatedIndex); + + // See which records are actually new + final oldRecords = Map.fromEntries( + linkedRecords.map((lr) => MapEntry(lr.key, lr))); + final newRecords = {}; + final sameRecords = {}; + final updatedLinkedRecords = []; + try { + for (var n = 0; n < updatedLinkedKeys.length; n++) { + final newKey = updatedLinkedKeys[n]; + final oldRecord = oldRecords[newKey]; + if (oldRecord == null) { + // Open the new record + final newRecord = await _openLinkedRecord(newKey); + newRecords[newKey] = newRecord; + updatedLinkedRecords.add(newRecord); + } else { + sameRecords[newKey] = oldRecord; + updatedLinkedRecords.add(oldRecord); + } + } + } on Exception catch (_) { + // On any exception close the records we have opened + await Future.wait(newRecords.entries.map((e) => e.value.close())); + rethrow; + } + + // From this point forward we should not throw an exception or everything + // is possibly invalid. Just pass the exception up it happens and the caller + // will have to delete this short array and reopen it if it can + await Future.wait(oldRecords.entries + .where((e) => !sameRecords.containsKey(e.key)) + .map((e) => e.value.close())); + + // Get the localseqs list from inspect results + final localReports = await [headRecord, ...updatedLinkedRecords].map((r) { + final start = (r.key == headRecord.key) ? 1 : 0; + return r + .inspect(subkeys: [ValueSubkeyRange.make(start, start + stride - 1)]); + }).wait; + final updatedLocalSeqs = + localReports.map((l) => l.localSeqs).expand((e) => e).toList(); + + // Make the new head cache + linkedRecords = updatedLinkedRecords; + index = updatedIndex; + free = updatedFree; + seqs = updatedSeqs; + localSeqs = updatedLocalSeqs; + } + + /// Pull the latest or updated copy of the head record from the network + Future _refreshInner( + {bool forceRefresh = true, bool onlyUpdates = false}) async { + // Get an updated head record copy if one exists + final head = await headRecord.getProtobuf(proto.DHTShortArray.fromBuffer, + subkey: 0, forceRefresh: forceRefresh, onlyUpdates: onlyUpdates); + if (head == null) { + if (onlyUpdates) { + // No update + return false; + } + throw StateError('head missing during refresh'); + } + + await _updateHead(head); + + return true; + } + + void _calculateStride() { + switch (headRecord.schema) { + case DHTSchemaDFLT(oCnt: final oCnt): + if (oCnt <= 1) { + throw StateError('Invalid DFLT schema in DHTShortArray'); + } + stride = oCnt - 1; + case DHTSchemaSMPL(oCnt: final oCnt, members: final members): + if (oCnt != 0 || members.length != 1 || members[0].mCnt <= 1) { + throw StateError('Invalid SMPL schema in DHTShortArray'); + } + stride = members[0].mCnt - 1; + } + assert(stride <= DHTShortArray.maxElements, 'stride too long'); + } + + DHTRecord? getLinkedRecord(int recordNumber) { + if (recordNumber == 0) { + return headRecord; + } + recordNumber--; + if (recordNumber >= linkedRecords.length) { + return null; + } + return linkedRecords[recordNumber]; + } + + Future getOrCreateLinkedRecord(int recordNumber) async { + if (recordNumber == 0) { + return headRecord; + } + final pool = DHTRecordPool.instance; + recordNumber--; + while (recordNumber >= linkedRecords.length) { + // Linked records must use SMPL schema so writer can be specified + // Use the same writer as the head record + final smplWriter = headRecord.writer!; + final parent = pool.getParentRecordKey(headRecord.key); + final routingContext = headRecord.routingContext; + final crypto = headRecord.crypto; + + final schema = DHTSchema.smpl( + oCnt: 0, + members: [DHTSchemaMember(mKey: smplWriter.key, mCnt: stride)]); + final dhtCreateRecord = await pool.create( + parent: parent, + routingContext: routingContext, + schema: schema, + crypto: crypto, + writer: smplWriter); + // Reopen with SMPL writer + await dhtCreateRecord.close(); + final dhtRecord = await pool.openWrite(dhtCreateRecord.key, smplWriter, + parent: parent, routingContext: routingContext, crypto: crypto); + + // Add to linked records + linkedRecords.add(dhtRecord); + if (!await _tryWriteHead()) { + await _refreshInner(); + } + } + return linkedRecords[recordNumber]; + } + + int emptyIndex() { + if (free.isNotEmpty) { + return free.removeLast(); + } + if (index.length == DHTShortArray.maxElements) { + throw StateError('too many elements'); + } + return index.length; + } + + void freeIndex(int idx) { + free.add(idx); + // xxx: free list optimization here? + } + + /// Validate the head from the DHT is properly formatted + /// and calculate the free list from it while we're here + List _makeFreeList( + List> linkedKeys, List index) { + // Ensure nothing is duplicated in the linked keys set + final newKeys = linkedKeys.toSet(); + assert( + newKeys.length <= (DHTShortArray.maxElements + (stride - 1)) ~/ stride, + 'too many keys'); + assert(newKeys.length == linkedKeys.length, 'duplicated linked keys'); + final newIndex = index.toSet(); + assert(newIndex.length <= DHTShortArray.maxElements, 'too many indexes'); + assert(newIndex.length == index.length, 'duplicated index locations'); + + // Ensure all the index keys fit into the existing records + final indexCapacity = (linkedKeys.length + 1) * stride; + int? maxIndex; + for (final idx in newIndex) { + assert(idx >= 0 || idx < indexCapacity, 'index out of range'); + if (maxIndex == null || idx > maxIndex) { + maxIndex = idx; + } + } + + // Figure out which indices are free + final free = []; + if (maxIndex != null) { + for (var i = 0; i < maxIndex; i++) { + if (!newIndex.contains(i)) { + free.add(i); + } + } + } + return free; + } + + /// Open a linked record for reading or writing, same as the head record + Future _openLinkedRecord(TypedKey recordKey) async { + final writer = headRecord.writer; + return (writer != null) + ? await DHTRecordPool.instance.openWrite( + recordKey, + writer, + parent: headRecord.key, + routingContext: headRecord.routingContext, + ) + : await DHTRecordPool.instance.openRead( + recordKey, + parent: headRecord.key, + routingContext: headRecord.routingContext, + ); + } + + /// Check if we know that the network has a copy of an index that is newer + /// than our local copy from looking at the seqs list in the head + bool indexNeedsRefresh(int index) { + // If our local sequence number is unknown or hasnt been written yet + // then a normal DHT operation is going to pull from the network anyway + if (localSeqs.length < index || localSeqs[index] == 0xFFFFFFFF) { + return false; + } + + // If the remote sequence number record is unknown or hasnt been written + // at this index yet, then we also do not refresh at this time as it + // is the first time the index is being written to + if (seqs.length < index || seqs[index] == 0xFFFFFFFF) { + return false; + } + + return localSeqs[index] < seqs[index]; + } + + /// Update the sequence number for a particular index in + /// our local sequence number list. + /// If a write is happening, update the network copy as well. + Future updateIndexSeq(int index, bool write) async { + final recordNumber = index ~/ stride; + final record = await getOrCreateLinkedRecord(recordNumber); + final recordSubkey = (index % stride) + ((recordNumber == 0) ? 1 : 0); + final report = + await record.inspect(subkeys: [ValueSubkeyRange.single(recordSubkey)]); + + while (localSeqs.length <= index) { + localSeqs.add(0xFFFFFFFF); + } + localSeqs[index] = report.localSeqs[0]; + if (write) { + while (seqs.length <= index) { + seqs.add(0xFFFFFFFF); + } + seqs[index] = report.localSeqs[0]; + } + } + + // Watch head for changes + Future _watch() async { + // This will update any existing watches if necessary + try { + await headRecord.watch(subkeys: [ValueSubkeyRange.single(0)]); + + // Update changes to the head record + // Don't watch for local changes because this class already handles + // notifying listeners and knows when it makes local changes + _subscription ??= + await headRecord.listen(localChanges: false, _onUpdateHead); + } on Exception { + // If anything fails, try to cancel the watches + await _cancelWatch(); + rethrow; + } + } + + // Stop watching for changes to head and linked records + Future _cancelWatch() async { + await headRecord.cancelWatch(); + await _subscription?.cancel(); + _subscription = null; + } + + // Called when a head or linked record changes + Future _onUpdateHead( + DHTRecord record, Uint8List? data, List subkeys) async { + // If head record subkey zero changes, then the layout + // of the dhtshortarray has changed + var updateHead = false; + if (record == headRecord && subkeys.containsSubkey(0)) { + updateHead = true; + } + + // If we have any other subkeys to update, do them first + final unord = List>.empty(growable: true); + for (final skr in subkeys) { + for (var subkey = skr.low; subkey <= skr.high; subkey++) { + // Skip head subkey + if (updateHead && subkey == 0) { + continue; + } + // Get the subkey, which caches the result in the local record store + unord.add(record.get(subkey: subkey, forceRefresh: true)); + } + } + await unord.wait; + + // Then update the head record + if (updateHead) { + await _refreshInner(forceRefresh: false); + } + } + + //////////////////////////////////////////////////////////////////////////// + + // Head/element mutex to ensure we keep the representation valid + final Mutex _headMutex = Mutex(); + // Subscription to head record internal changes + StreamSubscription? _subscription; + + // Head DHT record + final DHTRecord headRecord; + // How many elements per linked record + late final int stride; + +// List of additional records after the head record used for element data + List linkedRecords; + + // Ordering of the subkey indices. + // Elements are subkey numbers. Represents the element order. + List index; + // List of free subkeys for elements that have been removed. + // Used to optimize allocations. + List free; + // The sequence numbers of each subkey. + // Index is by subkey number not by element index. + // (n-1 for head record and then the next n for linked records) + List seqs; + // The local sequence numbers for each subkey. + List localSeqs; +} diff --git a/packages/veilid_support/lib/proto/dht.pb.dart b/packages/veilid_support/lib/proto/dht.pb.dart index 94d516b..7c96a7f 100644 --- a/packages/veilid_support/lib/proto/dht.pb.dart +++ b/packages/veilid_support/lib/proto/dht.pb.dart @@ -92,6 +92,7 @@ class DHTShortArray extends $pb.GeneratedMessage { static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'DHTShortArray', package: const $pb.PackageName(_omitMessageNames ? '' : 'dht'), createEmptyInstance: create) ..pc<$0.TypedKey>(1, _omitFieldNames ? '' : 'keys', $pb.PbFieldType.PM, subBuilder: $0.TypedKey.create) ..a<$core.List<$core.int>>(2, _omitFieldNames ? '' : 'index', $pb.PbFieldType.OY) + ..p<$core.int>(3, _omitFieldNames ? '' : 'seqs', $pb.PbFieldType.KU3) ..hasRequiredFields = false ; @@ -127,6 +128,9 @@ class DHTShortArray extends $pb.GeneratedMessage { $core.bool hasIndex() => $_has(1); @$pb.TagNumber(2) void clearIndex() => clearField(2); + + @$pb.TagNumber(3) + $core.List<$core.int> get seqs => $_getList(2); } class DHTLog extends $pb.GeneratedMessage { diff --git a/packages/veilid_support/lib/proto/dht.pbjson.dart b/packages/veilid_support/lib/proto/dht.pbjson.dart index 939cf65..bf31c30 100644 --- a/packages/veilid_support/lib/proto/dht.pbjson.dart +++ b/packages/veilid_support/lib/proto/dht.pbjson.dart @@ -36,13 +36,14 @@ const DHTShortArray$json = { '2': [ {'1': 'keys', '3': 1, '4': 3, '5': 11, '6': '.veilid.TypedKey', '10': 'keys'}, {'1': 'index', '3': 2, '4': 1, '5': 12, '10': 'index'}, + {'1': 'seqs', '3': 3, '4': 3, '5': 13, '10': 'seqs'}, ], }; /// Descriptor for `DHTShortArray`. Decode as a `google.protobuf.DescriptorProto`. final $typed_data.Uint8List dHTShortArrayDescriptor = $convert.base64Decode( 'Cg1ESFRTaG9ydEFycmF5EiQKBGtleXMYASADKAsyEC52ZWlsaWQuVHlwZWRLZXlSBGtleXMSFA' - 'oFaW5kZXgYAiABKAxSBWluZGV4'); + 'oFaW5kZXgYAiABKAxSBWluZGV4EhIKBHNlcXMYAyADKA1SBHNlcXM='); @$core.Deprecated('Use dHTLogDescriptor instead') const DHTLog$json = { diff --git a/packages/veilid_support/lib/src/identity.freezed.dart b/packages/veilid_support/lib/src/identity.freezed.dart index 9948cce..27f34ea 100644 --- a/packages/veilid_support/lib/src/identity.freezed.dart +++ b/packages/veilid_support/lib/src/identity.freezed.dart @@ -12,7 +12,7 @@ part of 'identity.dart'; T _$identity(T value) => value; final _privateConstructorUsedError = UnsupportedError( - 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#custom-getters-and-methods'); + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); AccountRecordInfo _$AccountRecordInfoFromJson(Map json) { return _AccountRecordInfo.fromJson(json); diff --git a/packages/veilid_support/lib/src/veilid_log.dart b/packages/veilid_support/lib/src/veilid_log.dart index fbabc70..0007754 100644 --- a/packages/veilid_support/lib/src/veilid_log.dart +++ b/packages/veilid_support/lib/src/veilid_log.dart @@ -33,6 +33,10 @@ void setVeilidLogLevel(LogLevel? level) { Veilid.instance.changeLogLevel('all', convertToVeilidConfigLogLevel(level)); } +void changeVeilidLogIgnore(String change) { + Veilid.instance.changeLogIgnore('all', change.split(',')); +} + class VeilidLoggy implements LoggyType { @override Loggy get loggy => Loggy('Veilid'); From 5a8b1caf93fc0c3047057d350943adb0d31e6ae2 Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Thu, 28 Mar 2024 21:46:26 -0500 Subject: [PATCH 63/68] more locking work --- .../new_account_page/new_account_page.dart | 2 + lib/chat_list/cubits/chat_list_cubit.dart | 24 +- .../cubits/contact_invitation_list_cubit.dart | 45 +- lib/contacts/cubits/contact_list_cubit.dart | 30 +- lib/main.dart | 17 +- .../src/dht_record/dht_record.dart | 22 +- .../src/dht_record/dht_record_pool.dart | 143 +++--- .../src/dht_short_array/dht_short_array.dart | 141 ++---- .../dht_short_array/dht_short_array_head.dart | 449 ++++++++++-------- packages/veilid_support/lib/src/identity.dart | 2 +- packages/veilid_support/pubspec.lock | 22 +- pubspec.lock | 68 +-- 12 files changed, 481 insertions(+), 484 deletions(-) diff --git a/lib/account_manager/views/new_account_page/new_account_page.dart b/lib/account_manager/views/new_account_page/new_account_page.dart index b249a99..f81f30d 100644 --- a/lib/account_manager/views/new_account_page/new_account_page.dart +++ b/lib/account_manager/views/new_account_page/new_account_page.dart @@ -54,12 +54,14 @@ class NewAccountPageState extends State { validator: FormBuilderValidators.compose([ FormBuilderValidators.required(), ]), + textInputAction: TextInputAction.next, ), FormBuilderTextField( name: formFieldPronouns, maxLength: 64, decoration: InputDecoration( labelText: translate('account.form_pronouns')), + textInputAction: TextInputAction.next, ), Row(children: [ const Spacer(), diff --git a/lib/chat_list/cubits/chat_list_cubit.dart b/lib/chat_list/cubits/chat_list_cubit.dart index 420dfbf..0591b7e 100644 --- a/lib/chat_list/cubits/chat_list_cubit.dart +++ b/lib/chat_list/cubits/chat_list_cubit.dart @@ -83,12 +83,10 @@ class ChatListCubit extends DHTShortArrayCubit { Future deleteChat( {required TypedKey remoteConversationRecordKey}) async { final remoteConversationKey = remoteConversationRecordKey.toProto(); - final accountRecordKey = - _activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey; // Remove Chat from account's list // if this fails, don't keep retrying, user can try again later - await operate((shortArray) async { + final deletedItem = await operate((shortArray) async { if (activeChatCubit.state == remoteConversationRecordKey) { activeChatCubit.setActiveChat(null); } @@ -101,19 +99,21 @@ class ChatListCubit extends DHTShortArrayCubit { if (c.remoteConversationRecordKey == remoteConversationKey) { // Found the right chat if (await shortArray.tryRemoveItem(i) != null) { - try { - await (await DHTShortArray.openOwned( - c.reconciledChatRecord.toVeilid(), - parent: accountRecordKey)) - .delete(); - } on Exception catch (e) { - log.debug('error removing reconciled chat record: $e', e); - } + return c; } - return; + return null; } } + return null; }); + if (deletedItem != null) { + try { + await DHTRecordPool.instance + .delete(deletedItem.reconciledChatRecord.toVeilid().recordKey); + } on Exception catch (e) { + log.debug('error removing reconciled chat record: $e', e); + } + } } final ActiveChatCubit activeChatCubit; diff --git a/lib/contact_invitation/cubits/contact_invitation_list_cubit.dart b/lib/contact_invitation/cubits/contact_invitation_list_cubit.dart index 4ffd2bd..37aafb9 100644 --- a/lib/contact_invitation/cubits/contact_invitation_list_cubit.dart +++ b/lib/contact_invitation/cubits/contact_invitation_list_cubit.dart @@ -6,6 +6,7 @@ import 'package:veilid_support/veilid_support.dart'; import '../../account_manager/account_manager.dart'; import '../../proto/proto.dart' as proto; +import '../../tools/tools.dart'; import '../models/models.dart'; ////////////////////////////////////////////////// @@ -157,7 +158,7 @@ class ContactInvitationListCubit _activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey; // Remove ContactInvitationRecord from account's list - await operate((shortArray) async { + final deletedItem = await operate((shortArray) async { for (var i = 0; i < shortArray.length; i++) { final item = await shortArray.getItemProtobuf( proto.ContactInvitationRecord.fromBuffer, i); @@ -166,25 +167,37 @@ class ContactInvitationListCubit } if (item.contactRequestInbox.recordKey.toVeilid() == contactRequestInboxRecordKey) { - await shortArray.tryRemoveItem(i); - - await (await pool.openOwned(item.contactRequestInbox.toVeilid(), - 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.openRead( - item.localConversationRecordKey.toVeilid(), - parent: accountRecordKey)) - .delete(); + if (await shortArray.tryRemoveItem(i) != null) { + return item; } - return; + return null; } } + return null; }); + + if (deletedItem != null) { + // Delete the contact request inbox + final contactRequestInbox = deletedItem.contactRequestInbox.toVeilid(); + await (await pool.openOwned(contactRequestInbox, + parent: accountRecordKey)) + .scope((contactRequestInbox) async { + // Wipe out old invitation so it shows up as invalid + await contactRequestInbox.tryWriteBytes(Uint8List(0)); + }); + try { + await pool.delete(contactRequestInbox.recordKey); + } on Exception catch (e) { + log.debug('error removing contact request inbox: $e', e); + } + if (!accepted) { + try { + await pool.delete(deletedItem.localConversationRecordKey.toVeilid()); + } on Exception catch (e) { + log.debug('error removing local conversation record: $e', e); + } + } + } } Future validateInvitation( diff --git a/lib/contacts/cubits/contact_list_cubit.dart b/lib/contacts/cubits/contact_list_cubit.dart index af66ac7..7f23475 100644 --- a/lib/contacts/cubits/contact_list_cubit.dart +++ b/lib/contacts/cubits/contact_list_cubit.dart @@ -14,8 +14,7 @@ class ContactListCubit extends DHTShortArrayCubit { ContactListCubit({ required ActiveAccountInfo activeAccountInfo, required proto.Account account, - }) : _activeAccountInfo = activeAccountInfo, - super( + }) : super( open: () => _open(activeAccountInfo, account), decodeElement: proto.Contact.fromBuffer); @@ -62,14 +61,12 @@ class ContactListCubit extends DHTShortArrayCubit { Future deleteContact({required proto.Contact contact}) async { final pool = DHTRecordPool.instance; - final accountRecordKey = - _activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey; final localConversationKey = contact.localConversationRecordKey.toVeilid(); final remoteConversationKey = contact.remoteConversationRecordKey.toVeilid(); // Remove Contact from account's list - await operate((shortArray) async { + final deletedItem = await operate((shortArray) async { for (var i = 0; i < shortArray.length; i++) { final item = await shortArray.getItemProtobuf(proto.Contact.fromBuffer, i); @@ -78,29 +75,28 @@ class ContactListCubit extends DHTShortArrayCubit { } if (item.remoteConversationRecordKey == contact.remoteConversationRecordKey) { - await shortArray.tryRemoveItem(i); - break; + if (await shortArray.tryRemoveItem(i) != null) { + return item; + } + return null; } } + return null; + }); + + if (deletedItem != null) { try { - await (await pool.openRead(localConversationKey, - parent: accountRecordKey)) - .delete(); + await pool.delete(localConversationKey); } on Exception catch (e) { log.debug('error removing local conversation record key: $e', e); } try { if (localConversationKey != remoteConversationKey) { - await (await pool.openRead(remoteConversationKey, - parent: accountRecordKey)) - .delete(); + await pool.delete(remoteConversationKey); } } on Exception catch (e) { log.debug('error removing remote conversation record key: $e', e); } - }); + } } - - // - final ActiveAccountInfo _activeAccountInfo; } diff --git a/lib/main.dart b/lib/main.dart index 49c3cb7..64ee506 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -27,8 +27,7 @@ void main() async { // Ansi colors ansiColorDisabled = false; - // Catch errors - await runZonedGuarded(() async { + Future mainFunc() async { // Logs initLoggy(); @@ -53,7 +52,15 @@ void main() async { // Hot reloads will only restart this part, not Veilid runApp(LocalizedApp(localizationDelegate, VeilidChatApp(initialThemeData: initialThemeData))); - }, (error, stackTrace) { - log.error('Dart Runtime: {$error}\n{$stackTrace}'); - }); + } + + if (kDebugMode) { + // In debug mode, run the app without catching exceptions for debugging + await mainFunc(); + } else { + // Catch errors in production without killing the app + await runZonedGuarded(mainFunc, (error, stackTrace) { + log.error('Dart Runtime: {$error}\n{$stackTrace}'); + }); + } } diff --git a/packages/veilid_support/lib/dht_support/src/dht_record/dht_record.dart b/packages/veilid_support/lib/dht_support/src/dht_record/dht_record.dart index 713b076..b5ae275 100644 --- a/packages/veilid_support/lib/dht_support/src/dht_record/dht_record.dart +++ b/packages/veilid_support/lib/dht_support/src/dht_record/dht_record.dart @@ -27,7 +27,6 @@ class DHTRecord { _defaultSubkey = defaultSubkey, _writer = writer, _open = true, - _valid = true, _sharedDHTRecordData = sharedDHTRecordData; final SharedDHTRecordData _sharedDHTRecordData; @@ -37,7 +36,6 @@ class DHTRecord { final DHTRecordCrypto _crypto; bool _open; - bool _valid; @internal StreamController? watchController; @internal @@ -59,9 +57,6 @@ class DHTRecord { OwnedDHTRecordPointer(recordKey: key, owner: ownerKeyPair!); Future close() async { - if (!_valid) { - throw StateError('already deleted'); - } if (!_open) { return; } @@ -70,33 +65,26 @@ class DHTRecord { _open = false; } - void _markDeleted() { - _valid = false; - } - - Future delete() => DHTRecordPool.instance.delete(key); - Future scope(Future Function(DHTRecord) scopeFunction) async { try { return await scopeFunction(this); } finally { - if (_valid) { - await close(); - } + await close(); } } Future deleteScope(Future Function(DHTRecord) scopeFunction) async { try { final out = await scopeFunction(this); - if (_valid && _open) { + if (_open) { await close(); } return out; } on Exception catch (_) { - if (_valid) { - await delete(); + if (_open) { + await close(); } + await DHTRecordPool.instance.delete(key); rethrow; } } diff --git a/packages/veilid_support/lib/dht_support/src/dht_record/dht_record_pool.dart b/packages/veilid_support/lib/dht_support/src/dht_record/dht_record_pool.dart index cce18c7..b78109b 100644 --- a/packages/veilid_support/lib/dht_support/src/dht_record/dht_record_pool.dart +++ b/packages/veilid_support/lib/dht_support/src/dht_record/dht_record_pool.dart @@ -73,6 +73,7 @@ class SharedDHTRecordData { VeilidRoutingContext defaultRoutingContext; Map subkeySeqCache = {}; bool needsWatchStateUpdate = false; + bool deleteOnClose = false; } // Per opened record data @@ -182,7 +183,7 @@ class DHTRecordPool with TableDBBacked { // If we are opening a key that already exists // make sure we are using the same parent if one was specified - _validateParent(parent, recordKey); + _validateParentInner(parent, recordKey); // See if this has been opened yet final openedRecordInfo = _opened[recordKey]; @@ -232,54 +233,58 @@ class DHTRecordPool with TableDBBacked { } if (openedRecordInfo.records.isEmpty) { await _routingContext.closeDHTRecord(key); + if (openedRecordInfo.shared.deleteOnClose) { + await _deleteInner(key); + } _opened.remove(key); } }); } - Future delete(TypedKey recordKey) async { - final allDeletedRecords = {}; - final allDeletedRecordKeys = []; + // Collect all dependencies (including the record itself) + // in reverse (bottom-up/delete order) + List _collectChildrenInner(TypedKey recordKey) { + assert(_mutex.isLocked, 'should be locked here'); - await _mutex.protect(() async { - // Collect all dependencies (including the record itself) - final allDeps = []; - final currentDeps = [recordKey]; - while (currentDeps.isNotEmpty) { - final nextDep = currentDeps.removeLast(); + final allDeps = []; + final currentDeps = [recordKey]; + while (currentDeps.isNotEmpty) { + final nextDep = currentDeps.removeLast(); - // Remove this child from its parent - await _removeDependencyInner(nextDep); - - allDeps.add(nextDep); - final childDeps = - _state.childrenByParent[nextDep.toJson()]?.toList() ?? []; - currentDeps.addAll(childDeps); - } - - // Delete all dependent records in parallel (including the record itself) - for (final dep in allDeps) { - // If record is opened, close it first - final openinfo = _opened[dep]; - if (openinfo != null) { - for (final rec in openinfo.records) { - allDeletedRecords.add(rec); - } - } - // Then delete - allDeletedRecordKeys.add(dep); - } - }); - - await Future.wait(allDeletedRecords.map((r) => r.close())); - for (final deletedRecord in allDeletedRecords) { - deletedRecord._markDeleted(); + allDeps.add(nextDep); + final childDeps = + _state.childrenByParent[nextDep.toJson()]?.toList() ?? []; + currentDeps.addAll(childDeps); } - await Future.wait( - allDeletedRecordKeys.map(_routingContext.deleteDHTRecord)); + return allDeps.reversedView; } - void _validateParent(TypedKey? parent, TypedKey child) { + Future _deleteInner(TypedKey recordKey) async { + // Remove this child from parents + await _removeDependenciesInner([recordKey]); + await _routingContext.deleteDHTRecord(recordKey); + } + + Future delete(TypedKey recordKey) async { + await _mutex.protect(() async { + final allDeps = _collectChildrenInner(recordKey); + + assert(allDeps.singleOrNull == recordKey, 'must delete children first'); + + final ori = _opened[recordKey]; + if (ori != null) { + // delete after close + ori.shared.deleteOnClose = true; + } else { + // delete now + await _deleteInner(recordKey); + } + }); + } + + void _validateParentInner(TypedKey? parent, TypedKey child) { + assert(_mutex.isLocked, 'should be locked here'); + final childJson = child.toJson(); final existingParent = _state.parentByChild[childJson]; if (parent == null) { @@ -319,29 +324,35 @@ class DHTRecordPool with TableDBBacked { } } - Future _removeDependencyInner(TypedKey child) async { + Future _removeDependenciesInner(List childList) async { assert(_mutex.isLocked, 'should be locked here'); - if (_state.rootRecords.contains(child)) { - _state = await store( - _state.copyWith(rootRecords: _state.rootRecords.remove(child))); - } else { - final parent = _state.parentByChild[child.toJson()]; - if (parent == null) { - return; - } - final children = _state.childrenByParent[parent.toJson()]!.remove(child); - late final DHTRecordPoolAllocations newState; - if (children.isEmpty) { - newState = _state.copyWith( - childrenByParent: _state.childrenByParent.remove(parent.toJson()), - parentByChild: _state.parentByChild.remove(child.toJson())); + + var state = _state; + + for (final child in childList) { + if (_state.rootRecords.contains(child)) { + state = state.copyWith(rootRecords: state.rootRecords.remove(child)); } else { - newState = _state.copyWith( - childrenByParent: - _state.childrenByParent.add(parent.toJson(), children), - parentByChild: _state.parentByChild.remove(child.toJson())); + final parent = state.parentByChild[child.toJson()]; + if (parent == null) { + continue; + } + final children = state.childrenByParent[parent.toJson()]!.remove(child); + if (children.isEmpty) { + state = state.copyWith( + childrenByParent: state.childrenByParent.remove(parent.toJson()), + parentByChild: state.parentByChild.remove(child.toJson())); + } else { + state = state.copyWith( + childrenByParent: + state.childrenByParent.add(parent.toJson(), children), + parentByChild: state.parentByChild.remove(child.toJson())); + } } - _state = await store(newState); + } + + if (state != _state) { + _state = await store(state); } } @@ -595,10 +606,10 @@ class DHTRecordPool with TableDBBacked { _tickCount = 0; try { - // See if any opened records need watch state changes - final unord = Function()>[]; + final allSuccess = await _mutex.protect(() async { + // See if any opened records need watch state changes + final unord = Function()>[]; - await _mutex.protect(() async { for (final kv in _opened.entries) { final openedRecordKey = kv.key; final openedRecordInfo = kv.value; @@ -647,13 +658,15 @@ class DHTRecordPool with TableDBBacked { } } } + + // Process all watch changes + return unord.isEmpty || + (await unord.map((f) => f()).wait).reduce((a, b) => a && b); }); - // Process all watch changes // If any watched did not success, back off the attempts to // update the watches for a bit - final allSuccess = unord.isEmpty || - (await unord.map((f) => f()).wait).reduce((a, b) => a && b); + if (!allSuccess) { _watchBackoffTimer *= watchBackoffMultiplier; _watchBackoffTimer = min(_watchBackoffTimer, watchBackoffMax); diff --git a/packages/veilid_support/lib/dht_support/src/dht_short_array/dht_short_array.dart b/packages/veilid_support/lib/dht_support/src/dht_short_array/dht_short_array.dart index 8136a61..2fca99e 100644 --- a/packages/veilid_support/lib/dht_support/src/dht_short_array/dht_short_array.dart +++ b/packages/veilid_support/lib/dht_support/src/dht_short_array/dht_short_array.dart @@ -16,7 +16,11 @@ class DHTShortArray { // Constructors DHTShortArray._({required DHTRecord headRecord}) - : _head = _DHTShortArrayHead(headRecord: headRecord) {} + : _head = _DHTShortArrayHead(headRecord: headRecord) { + _head.onUpdatedHead = () { + _watchController?.sink.add(null); + }; + } // Create a DHTShortArray // if smplWriter is specified, uses a SMPL schema with a single writer @@ -52,12 +56,15 @@ class DHTShortArray { try { final dhtShortArray = DHTShortArray._(headRecord: dhtRecord); - if (!await dhtShortArray._head._tryWriteHead()) { - throw StateError('Failed to write head at this time'); - } + await dhtShortArray._head.operate((head) async { + if (!await head._writeHead()) { + throw StateError('Failed to write head at this time'); + } + }); return dhtShortArray; } on Exception catch (_) { - await dhtRecord.delete(); + await dhtRecord.close(); + await pool.delete(dhtRecord.key); rethrow; } } @@ -70,7 +77,7 @@ class DHTShortArray { parent: parent, routingContext: routingContext, crypto: crypto); try { final dhtShortArray = DHTShortArray._(headRecord: dhtRecord); - await dhtShortArray._head._refreshInner(); + await dhtShortArray._head.operate((head) => head._loadHead()); return dhtShortArray; } on Exception catch (_) { await dhtRecord.close(); @@ -90,7 +97,7 @@ class DHTShortArray { parent: parent, routingContext: routingContext, crypto: crypto); try { final dhtShortArray = DHTShortArray._(headRecord: dhtRecord); - await dhtShortArray._head._refreshInner(); + await dhtShortArray._head.operate((head) => head._loadHead()); return dhtShortArray; } on Exception catch (_) { await dhtRecord.close(); @@ -115,13 +122,14 @@ class DHTShortArray { //////////////////////////////////////////////////////////////////////////// // Public API - // External references for the shortarray - TypedKey get recordKey => _head.headRecord.key; - OwnedDHTRecordPointer get recordPointer => - _head.headRecord.ownedDHTRecordPointer; + /// Get the record key for this shortarray + TypedKey get recordKey => _head.recordKey; + + /// Get the record pointer foir this shortarray + OwnedDHTRecordPointer get recordPointer => _head.recordPointer; /// Returns the number of elements in the DHTShortArray - int get length => _head.index.length; + int get length => _head.length; /// Free all resources for the DHTShortArray Future close() async { @@ -131,8 +139,8 @@ class DHTShortArray { /// Free all resources for the DHTShortArray and delete it from the DHT Future delete() async { - await _watchController?.close(); - await _head.delete(); + await close(); + await DHTRecordPool.instance.delete(recordKey); } /// Runs a closure that guarantees the DHTShortArray @@ -169,23 +177,15 @@ class DHTShortArray { Future _getItemInner(_DHTShortArrayHead head, int pos, {bool forceRefresh = false}) async { - if (pos < 0 || pos >= head.index.length) { - throw IndexError.withLength(pos, head.index.length); + if (pos < 0 || pos >= length) { + throw IndexError.withLength(pos, length); } - final index = head.index[pos]; - final recordNumber = index ~/ head.stride; - final record = head.getLinkedRecord(recordNumber); - if (record == null) { - throw StateError('Record does not exist'); - } - final recordSubkey = (index % head.stride) + ((recordNumber == 0) ? 1 : 0); - - final refresh = forceRefresh || head.indexNeedsRefresh(index); + final (record, recordSubkey) = await head.lookupPosition(pos); + final refresh = forceRefresh || head.positionNeedsRefresh(pos); final out = record.get(subkey: recordSubkey, forceRefresh: refresh); - - await head.updateIndexSeq(index, false); + await head.updatePositionSeq(pos, false); return out; } @@ -197,7 +197,7 @@ class DHTShortArray { _head.operate((head) async { final out = []; - for (var pos = 0; pos < head.index.length; pos++) { + for (var pos = 0; pos < head.length; pos++) { final elem = await _getItemInner(head, pos, forceRefresh: forceRefresh); if (elem == null) { @@ -248,21 +248,14 @@ class DHTShortArray { final out = await _head .operateWrite((head) async => _tryAddItemInner(head, value)) ?? false; - - // Send update - _watchController?.sink.add(null); - return out; } Future _tryAddItemInner( _DHTShortArrayHead head, Uint8List value) async { - // Allocate empty index - final index = head.emptyIndex(); - - // Add new index - final pos = head.index.length; - head.index.add(index); + // Allocate empty index at the end of the list + final pos = head.length; + head.allocateIndex(pos); // Write item final (_, wasSet) = await _tryWriteItemInner(head, pos, value); @@ -271,7 +264,7 @@ class DHTShortArray { } // Get sequence number written - await head.updateIndexSeq(index, true); + await head.updatePositionSeq(pos, true); return true; } @@ -287,19 +280,13 @@ class DHTShortArray { (head) async => _tryInsertItemInner(head, pos, value)) ?? false; - // Send update - _watchController?.sink.add(null); - return out; } Future _tryInsertItemInner( _DHTShortArrayHead head, int pos, Uint8List value) async { - // Allocate empty index - final index = head.emptyIndex(); - - // Add new index - _head.index.insert(pos, index); + // Allocate empty index at position + head.allocateIndex(pos); // Write item final (_, wasSet) = await _tryWriteItemInner(head, pos, value); @@ -308,7 +295,7 @@ class DHTShortArray { } // Get sequence number written - await head.updateIndexSeq(index, true); + await head.updatePositionSeq(pos, true); return true; } @@ -325,24 +312,13 @@ class DHTShortArray { (head) async => _trySwapItemInner(head, aPos, bPos)) ?? false; - // Send update - _watchController?.sink.add(null); - return out; } Future _trySwapItemInner( _DHTShortArrayHead head, int aPos, int bPos) async { - // No-op case - if (aPos == bPos) { - return true; - } - // Swap indices - final aIdx = _head.index[aPos]; - final bIdx = _head.index[bPos]; - _head.index[aPos] = bIdx; - _head.index[bPos] = aIdx; + head.swapIndex(aPos, bPos); return true; } @@ -358,28 +334,17 @@ class DHTShortArray { final out = _head.operateWrite((head) async => _tryRemoveItemInner(head, pos)); - // Send update - _watchController?.sink.add(null); - return out; } Future _tryRemoveItemInner( _DHTShortArrayHead head, int pos) async { - final index = _head.index.removeAt(pos); - final recordNumber = index ~/ head.stride; - final record = head.getLinkedRecord(recordNumber); - if (record == null) { - throw StateError('Record does not exist'); - } - final recordSubkey = (index % head.stride) + ((recordNumber == 0) ? 1 : 0); - + final (record, recordSubkey) = await head.lookupPosition(pos); final result = await record.get(subkey: recordSubkey); if (result == null) { throw StateError('Element does not exist'); } - - head.freeIndex(index); + head.freeIndex(pos); return result; } @@ -405,15 +370,11 @@ class DHTShortArray { final out = await _head.operateWrite((head) async => _tryClearInner(head)) ?? false; - // Send update - _watchController?.sink.add(null); - return out; } Future _tryClearInner(_DHTShortArrayHead head) async { - head.index.clear(); - head.free.clear(); + head.clearIndex(); return true; } @@ -434,23 +395,15 @@ class DHTShortArray { return (null, false); } - // Send update - _watchController?.sink.add(null); - return out; } Future<(Uint8List?, bool)> _tryWriteItemInner( _DHTShortArrayHead head, int pos, Uint8List newValue) async { - if (pos < 0 || pos >= head.index.length) { - throw IndexError.withLength(pos, _head.index.length); + if (pos < 0 || pos >= head.length) { + throw IndexError.withLength(pos, head.length); } - - final index = head.index[pos]; - final recordNumber = index ~/ head.stride; - final record = await head.getOrCreateLinkedRecord(recordNumber); - final recordSubkey = (index % head.stride) + ((recordNumber == 0) ? 1 : 0); - + final (record, recordSubkey) = await head.lookupPosition(pos); final oldValue = await record.get(subkey: recordSubkey); final result = await record.tryWriteBytes(newValue, subkey: recordSubkey); if (result != null) { @@ -471,9 +424,6 @@ class DHTShortArray { (_, wasSet) = await _tryWriteItemInner(head, pos, newValue); return wasSet; }, timeout: timeout); - - // Send update - _watchController?.sink.add(null); } /// Change an item at position 'pos' of the DHTShortArray. @@ -497,9 +447,6 @@ class DHTShortArray { (_, wasSet) = await _tryWriteItemInner(head, pos, updatedData); return wasSet; }, timeout: timeout); - - // Send update - _watchController?.sink.add(null); } /// Convenience function: @@ -569,13 +516,13 @@ class DHTShortArray { // rid of the controller and drop our subscriptions unawaited(_listenMutex.protect(() async { // Cancel watches of head record - await _head._cancelWatch(); + await _head.cancelWatch(); _watchController = null; })); }); // Start watching head record - await _head._watch(); + await _head.watch(); } // Return subscription return _watchController!.stream.listen((_) => onChanged()); diff --git a/packages/veilid_support/lib/dht_support/src/dht_short_array/dht_short_array_head.dart b/packages/veilid_support/lib/dht_support/src/dht_short_array/dht_short_array_head.dart index 94aac2c..708bb38 100644 --- a/packages/veilid_support/lib/dht_support/src/dht_short_array/dht_short_array_head.dart +++ b/packages/veilid_support/lib/dht_support/src/dht_short_array/dht_short_array_head.dart @@ -1,40 +1,52 @@ part of 'dht_short_array.dart'; -//////////////////////////////////////////////////////////////// -// Internal Operations - class _DHTShortArrayHead { - _DHTShortArrayHead({required this.headRecord}) - : linkedRecords = [], - index = [], - free = [], - seqs = [], - localSeqs = [] { + _DHTShortArrayHead({required DHTRecord headRecord}) + : _headRecord = headRecord, + _linkedRecords = [], + _index = [], + _free = [], + _seqs = [], + _localSeqs = [] { _calculateStride(); } - proto.DHTShortArray toProto() { + void _calculateStride() { + switch (_headRecord.schema) { + case DHTSchemaDFLT(oCnt: final oCnt): + if (oCnt <= 1) { + throw StateError('Invalid DFLT schema in DHTShortArray'); + } + _stride = oCnt - 1; + case DHTSchemaSMPL(oCnt: final oCnt, members: final members): + if (oCnt != 0 || members.length != 1 || members[0].mCnt <= 1) { + throw StateError('Invalid SMPL schema in DHTShortArray'); + } + _stride = members[0].mCnt - 1; + } + assert(_stride <= DHTShortArray.maxElements, 'stride too long'); + } + + proto.DHTShortArray _toProto() { + assert(_headMutex.isLocked, 'should be in mutex here'); + final head = proto.DHTShortArray(); - head.keys.addAll(linkedRecords.map((lr) => lr.key.toProto())); - head.index.addAll(index); - head.seqs.addAll(seqs); + head.keys.addAll(_linkedRecords.map((lr) => lr.key.toProto())); + head.index.addAll(_index); + head.seqs.addAll(_seqs); // Do not serialize free list, it gets recreated // Do not serialize local seqs, they are only locally relevant return head; } - Future close() async { - final futures = >[headRecord.close()]; - for (final lr in linkedRecords) { - futures.add(lr.close()); - } - await Future.wait(futures); - } + TypedKey get recordKey => _headRecord.key; + OwnedDHTRecordPointer get recordPointer => _headRecord.ownedDHTRecordPointer; + int get length => _index.length; - Future delete() async { - final futures = >[headRecord.delete()]; - for (final lr in linkedRecords) { - futures.add(lr.delete()); + Future close() async { + final futures = >[_headRecord.close()]; + for (final lr in _linkedRecords) { + futures.add(lr.close()); } await Future.wait(futures); } @@ -46,55 +58,57 @@ class _DHTShortArrayHead { }); Future operateWrite( - Future Function(_DHTShortArrayHead) closure) async { - final oldLinkedRecords = List.of(linkedRecords); - final oldIndex = List.of(index); - final oldFree = List.of(free); - final oldSeqs = List.of(seqs); - try { - final out = await _headMutex.protect(() async { - final out = await closure(this); - // Write head assuming it has been changed - if (!await _tryWriteHead()) { - // Failed to write head means head got overwritten so write should - // be considered failed - return null; - } - return out; - }); - return out; - } on Exception { - // Exception means state needs to be reverted - linkedRecords = oldLinkedRecords; - index = oldIndex; - free = oldFree; - seqs = oldSeqs; + Future Function(_DHTShortArrayHead) closure) async => + _headMutex.protect(() async { + final oldLinkedRecords = List.of(_linkedRecords); + final oldIndex = List.of(_index); + final oldFree = List.of(_free); + final oldSeqs = List.of(_seqs); + try { + final out = await closure(this); + // Write head assuming it has been changed + if (!await _writeHead()) { + // Failed to write head means head got overwritten so write should + // be considered failed + return null; + } - rethrow; - } - } + onUpdatedHead?.call(); + return out; + } on Exception { + // Exception means state needs to be reverted + _linkedRecords = oldLinkedRecords; + _index = oldIndex; + _free = oldFree; + _seqs = oldSeqs; + + rethrow; + } + }); Future operateWriteEventual( Future Function(_DHTShortArrayHead) closure, {Duration? timeout}) async { - late List oldLinkedRecords; - late List oldIndex; - late List oldFree; - late List oldSeqs; - final timeoutTs = timeout == null ? null : Veilid.instance.now().offset(TimestampDuration.fromDuration(timeout)); - try { - await _headMutex.protect(() async { + + await _headMutex.protect(() async { + late List oldLinkedRecords; + late List oldIndex; + late List oldFree; + late List oldSeqs; + + try { // Iterate until we have a successful element and head write + do { // Save off old values each pass of tryWriteHead because the head // will have changed - oldLinkedRecords = List.of(linkedRecords); - oldIndex = List.of(index); - oldFree = List.of(free); - oldSeqs = List.of(seqs); + oldLinkedRecords = List.of(_linkedRecords); + oldIndex = List.of(_index); + oldFree = List.of(_free); + oldSeqs = List.of(_seqs); // Try to do the element write do { @@ -107,26 +121,30 @@ class _DHTShortArrayHead { } while (!await closure(this)); // Try to do the head write - } while (!await _tryWriteHead()); - }); - } on Exception { - // Exception means state needs to be reverted - linkedRecords = oldLinkedRecords; - index = oldIndex; - free = oldFree; - seqs = oldSeqs; + } while (!await _writeHead()); - rethrow; - } + onUpdatedHead?.call(); + } on Exception { + // Exception means state needs to be reverted + _linkedRecords = oldLinkedRecords; + _index = oldIndex; + _free = oldFree; + _seqs = oldSeqs; + + rethrow; + } + }); } /// Serialize and write out the current head record, possibly updating it /// if a newer copy is available online. Returns true if the write was /// successful - Future _tryWriteHead() async { - final headBuffer = toProto().writeToBuffer(); + Future _writeHead() async { + assert(_headMutex.isLocked, 'should be in mutex here'); - final existingData = await headRecord.tryWriteBytes(headBuffer); + final headBuffer = _toProto().writeToBuffer(); + + final existingData = await _headRecord.tryWriteBytes(headBuffer); if (existingData != null) { // Head write failed, incorporate update await _updateHead(proto.DHTShortArray.fromBuffer(existingData)); @@ -138,6 +156,8 @@ class _DHTShortArrayHead { /// Validate a new head record that has come in from the network Future _updateHead(proto.DHTShortArray head) async { + assert(_headMutex.isLocked, 'should be in mutex here'); + // Get the set of new linked keys and validate it final updatedLinkedKeys = head.keys.map((p) => p.toVeilid()).toList(); final updatedIndex = List.of(head.index); @@ -146,7 +166,7 @@ class _DHTShortArrayHead { // See which records are actually new final oldRecords = Map.fromEntries( - linkedRecords.map((lr) => MapEntry(lr.key, lr))); + _linkedRecords.map((lr) => MapEntry(lr.key, lr))); final newRecords = {}; final sameRecords = {}; final updatedLinkedRecords = []; @@ -173,32 +193,33 @@ class _DHTShortArrayHead { // From this point forward we should not throw an exception or everything // is possibly invalid. Just pass the exception up it happens and the caller // will have to delete this short array and reopen it if it can - await Future.wait(oldRecords.entries + await oldRecords.entries .where((e) => !sameRecords.containsKey(e.key)) - .map((e) => e.value.close())); + .map((e) => e.value.close()) + .wait; // Get the localseqs list from inspect results - final localReports = await [headRecord, ...updatedLinkedRecords].map((r) { - final start = (r.key == headRecord.key) ? 1 : 0; - return r - .inspect(subkeys: [ValueSubkeyRange.make(start, start + stride - 1)]); + final localReports = await [_headRecord, ...updatedLinkedRecords].map((r) { + final start = (r.key == _headRecord.key) ? 1 : 0; + return r.inspect( + subkeys: [ValueSubkeyRange.make(start, start + _stride - 1)]); }).wait; final updatedLocalSeqs = localReports.map((l) => l.localSeqs).expand((e) => e).toList(); // Make the new head cache - linkedRecords = updatedLinkedRecords; - index = updatedIndex; - free = updatedFree; - seqs = updatedSeqs; - localSeqs = updatedLocalSeqs; + _linkedRecords = updatedLinkedRecords; + _index = updatedIndex; + _free = updatedFree; + _seqs = updatedSeqs; + _localSeqs = updatedLocalSeqs; } - /// Pull the latest or updated copy of the head record from the network - Future _refreshInner( + // Pull the latest or updated copy of the head record from the network + Future _loadHead( {bool forceRefresh = true, bool onlyUpdates = false}) async { // Get an updated head record copy if one exists - final head = await headRecord.getProtobuf(proto.DHTShortArray.fromBuffer, + final head = await _headRecord.getProtobuf(proto.DHTShortArray.fromBuffer, subkey: 0, forceRefresh: forceRefresh, onlyUpdates: onlyUpdates); if (head == null) { if (onlyUpdates) { @@ -213,82 +234,110 @@ class _DHTShortArrayHead { return true; } - void _calculateStride() { - switch (headRecord.schema) { - case DHTSchemaDFLT(oCnt: final oCnt): - if (oCnt <= 1) { - throw StateError('Invalid DFLT schema in DHTShortArray'); - } - stride = oCnt - 1; - case DHTSchemaSMPL(oCnt: final oCnt, members: final members): - if (oCnt != 0 || members.length != 1 || members[0].mCnt <= 1) { - throw StateError('Invalid SMPL schema in DHTShortArray'); - } - stride = members[0].mCnt - 1; - } - assert(stride <= DHTShortArray.maxElements, 'stride too long'); - } + ///////////////////////////////////////////////////////////////////////////// + // Linked record management - DHTRecord? getLinkedRecord(int recordNumber) { + Future _getOrCreateLinkedRecord(int recordNumber) async { if (recordNumber == 0) { - return headRecord; - } - recordNumber--; - if (recordNumber >= linkedRecords.length) { - return null; - } - return linkedRecords[recordNumber]; - } - - Future getOrCreateLinkedRecord(int recordNumber) async { - if (recordNumber == 0) { - return headRecord; + return _headRecord; } final pool = DHTRecordPool.instance; recordNumber--; - while (recordNumber >= linkedRecords.length) { + while (recordNumber >= _linkedRecords.length) { // Linked records must use SMPL schema so writer can be specified // Use the same writer as the head record - final smplWriter = headRecord.writer!; - final parent = pool.getParentRecordKey(headRecord.key); - final routingContext = headRecord.routingContext; - final crypto = headRecord.crypto; + final smplWriter = _headRecord.writer!; + final parent = _headRecord.key; + final routingContext = _headRecord.routingContext; + final crypto = _headRecord.crypto; final schema = DHTSchema.smpl( oCnt: 0, - members: [DHTSchemaMember(mKey: smplWriter.key, mCnt: stride)]); - final dhtCreateRecord = await pool.create( + members: [DHTSchemaMember(mKey: smplWriter.key, mCnt: _stride)]); + final dhtRecord = await pool.create( parent: parent, routingContext: routingContext, schema: schema, crypto: crypto, writer: smplWriter); - // Reopen with SMPL writer - await dhtCreateRecord.close(); - final dhtRecord = await pool.openWrite(dhtCreateRecord.key, smplWriter, - parent: parent, routingContext: routingContext, crypto: crypto); // Add to linked records - linkedRecords.add(dhtRecord); - if (!await _tryWriteHead()) { - await _refreshInner(); - } + _linkedRecords.add(dhtRecord); } - return linkedRecords[recordNumber]; + if (!await _writeHead()) { + throw StateError('failed to add linked record'); + } + return _linkedRecords[recordNumber]; } - int emptyIndex() { - if (free.isNotEmpty) { - return free.removeLast(); + /// Open a linked record for reading or writing, same as the head record + Future _openLinkedRecord(TypedKey recordKey) async { + final writer = _headRecord.writer; + return (writer != null) + ? await DHTRecordPool.instance.openWrite( + recordKey, + writer, + parent: _headRecord.key, + routingContext: _headRecord.routingContext, + ) + : await DHTRecordPool.instance.openRead( + recordKey, + parent: _headRecord.key, + routingContext: _headRecord.routingContext, + ); + } + + Future<(DHTRecord, int)> lookupPosition(int pos) async { + final idx = _index[pos]; + return lookupIndex(idx); + } + + Future<(DHTRecord, int)> lookupIndex(int idx) async { + final recordNumber = idx ~/ _stride; + final record = await _getOrCreateLinkedRecord(recordNumber); + final recordSubkey = (idx % _stride) + ((recordNumber == 0) ? 1 : 0); + return (record, recordSubkey); + } + + ///////////////////////////////////////////////////////////////////////////// + // Index management + + /// Allocate an empty index slot at a specific position + void allocateIndex(int pos) { + // Allocate empty index + final idx = _emptyIndex(); + _index.insert(pos, idx); + } + + int _emptyIndex() { + if (_free.isNotEmpty) { + return _free.removeLast(); } - if (index.length == DHTShortArray.maxElements) { + if (_index.length == DHTShortArray.maxElements) { throw StateError('too many elements'); } - return index.length; + return _index.length; } - void freeIndex(int idx) { - free.add(idx); + void swapIndex(int aPos, int bPos) { + if (aPos == bPos) { + return; + } + final aIdx = _index[aPos]; + final bIdx = _index[bPos]; + _index[aPos] = bIdx; + _index[bPos] = aIdx; + } + + void clearIndex() { + _index.clear(); + _free.clear(); + } + + /// Release an index at a particular position + void freeIndex(int pos) { + final idx = _index.removeAt(pos); + _free.add(idx); // xxx: free list optimization here? } @@ -299,7 +348,8 @@ class _DHTShortArrayHead { // Ensure nothing is duplicated in the linked keys set final newKeys = linkedKeys.toSet(); assert( - newKeys.length <= (DHTShortArray.maxElements + (stride - 1)) ~/ stride, + newKeys.length <= + (DHTShortArray.maxElements + (_stride - 1)) ~/ _stride, 'too many keys'); assert(newKeys.length == linkedKeys.length, 'duplicated linked keys'); final newIndex = index.toSet(); @@ -307,7 +357,7 @@ class _DHTShortArrayHead { assert(newIndex.length == index.length, 'duplicated index locations'); // Ensure all the index keys fit into the existing records - final indexCapacity = (linkedKeys.length + 1) * stride; + final indexCapacity = (linkedKeys.length + 1) * _stride; int? maxIndex; for (final idx in newIndex) { assert(idx >= 0 || idx < indexCapacity, 'index out of range'); @@ -328,117 +378,97 @@ class _DHTShortArrayHead { return free; } - /// Open a linked record for reading or writing, same as the head record - Future _openLinkedRecord(TypedKey recordKey) async { - final writer = headRecord.writer; - return (writer != null) - ? await DHTRecordPool.instance.openWrite( - recordKey, - writer, - parent: headRecord.key, - routingContext: headRecord.routingContext, - ) - : await DHTRecordPool.instance.openRead( - recordKey, - parent: headRecord.key, - routingContext: headRecord.routingContext, - ); - } - /// Check if we know that the network has a copy of an index that is newer /// than our local copy from looking at the seqs list in the head - bool indexNeedsRefresh(int index) { + bool positionNeedsRefresh(int pos) { + final idx = _index[pos]; + // If our local sequence number is unknown or hasnt been written yet // then a normal DHT operation is going to pull from the network anyway - if (localSeqs.length < index || localSeqs[index] == 0xFFFFFFFF) { + if (_localSeqs.length < idx || _localSeqs[idx] == 0xFFFFFFFF) { return false; } // If the remote sequence number record is unknown or hasnt been written // at this index yet, then we also do not refresh at this time as it // is the first time the index is being written to - if (seqs.length < index || seqs[index] == 0xFFFFFFFF) { + if (_seqs.length < idx || _seqs[idx] == 0xFFFFFFFF) { return false; } - return localSeqs[index] < seqs[index]; + return _localSeqs[idx] < _seqs[idx]; } /// Update the sequence number for a particular index in /// our local sequence number list. /// If a write is happening, update the network copy as well. - Future updateIndexSeq(int index, bool write) async { - final recordNumber = index ~/ stride; - final record = await getOrCreateLinkedRecord(recordNumber); - final recordSubkey = (index % stride) + ((recordNumber == 0) ? 1 : 0); + Future updatePositionSeq(int pos, bool write) async { + final idx = _index[pos]; + final (record, recordSubkey) = await lookupIndex(idx); final report = await record.inspect(subkeys: [ValueSubkeyRange.single(recordSubkey)]); - while (localSeqs.length <= index) { - localSeqs.add(0xFFFFFFFF); + while (_localSeqs.length <= idx) { + _localSeqs.add(0xFFFFFFFF); } - localSeqs[index] = report.localSeqs[0]; + _localSeqs[idx] = report.localSeqs[0]; if (write) { - while (seqs.length <= index) { - seqs.add(0xFFFFFFFF); + while (_seqs.length <= idx) { + _seqs.add(0xFFFFFFFF); } - seqs[index] = report.localSeqs[0]; + _seqs[idx] = report.localSeqs[0]; } } + ///////////////////////////////////////////////////////////////////////////// + // Watch For Updates + // Watch head for changes - Future _watch() async { + Future watch() async { // This will update any existing watches if necessary try { - await headRecord.watch(subkeys: [ValueSubkeyRange.single(0)]); + await _headRecord.watch(subkeys: [ValueSubkeyRange.single(0)]); // Update changes to the head record // Don't watch for local changes because this class already handles // notifying listeners and knows when it makes local changes _subscription ??= - await headRecord.listen(localChanges: false, _onUpdateHead); + await _headRecord.listen(localChanges: false, _onHeadValueChanged); } on Exception { // If anything fails, try to cancel the watches - await _cancelWatch(); + await cancelWatch(); rethrow; } } // Stop watching for changes to head and linked records - Future _cancelWatch() async { - await headRecord.cancelWatch(); + Future cancelWatch() async { + await _headRecord.cancelWatch(); await _subscription?.cancel(); _subscription = null; } - // Called when a head or linked record changes - Future _onUpdateHead( + Future _onHeadValueChanged( DHTRecord record, Uint8List? data, List subkeys) async { // If head record subkey zero changes, then the layout // of the dhtshortarray has changed - var updateHead = false; - if (record == headRecord && subkeys.containsSubkey(0)) { - updateHead = true; + if (data == null) { + throw StateError('head value changed without data'); + } + if (record.key != _headRecord.key || + subkeys.length != 1 || + subkeys[0] != ValueSubkeyRange.single(0)) { + throw StateError('watch returning wrong subkey range'); } - // If we have any other subkeys to update, do them first - final unord = List>.empty(growable: true); - for (final skr in subkeys) { - for (var subkey = skr.low; subkey <= skr.high; subkey++) { - // Skip head subkey - if (updateHead && subkey == 0) { - continue; - } - // Get the subkey, which caches the result in the local record store - unord.add(record.get(subkey: subkey, forceRefresh: true)); - } - } - await unord.wait; + // Decode updated head + final headData = proto.DHTShortArray.fromBuffer(data); // Then update the head record - if (updateHead) { - await _refreshInner(forceRefresh: false); - } + await _headMutex.protect(() async { + await _updateHead(headData); + onUpdatedHead?.call(); + }); } //////////////////////////////////////////////////////////////////////////// @@ -447,25 +477,26 @@ class _DHTShortArrayHead { final Mutex _headMutex = Mutex(); // Subscription to head record internal changes StreamSubscription? _subscription; + // Notify closure for external head changes + void Function()? onUpdatedHead; // Head DHT record - final DHTRecord headRecord; + final DHTRecord _headRecord; // How many elements per linked record - late final int stride; - -// List of additional records after the head record used for element data - List linkedRecords; + late final int _stride; + // List of additional records after the head record used for element data + List _linkedRecords; // Ordering of the subkey indices. // Elements are subkey numbers. Represents the element order. - List index; + List _index; // List of free subkeys for elements that have been removed. // Used to optimize allocations. - List free; + List _free; // The sequence numbers of each subkey. // Index is by subkey number not by element index. // (n-1 for head record and then the next n for linked records) - List seqs; + List _seqs; // The local sequence numbers for each subkey. - List localSeqs; + List _localSeqs; } diff --git a/packages/veilid_support/lib/src/identity.dart b/packages/veilid_support/lib/src/identity.dart index 63361c7..e9ad6b6 100644 --- a/packages/veilid_support/lib/src/identity.dart +++ b/packages/veilid_support/lib/src/identity.dart @@ -93,7 +93,7 @@ extension IdentityMasterExtension on IdentityMaster { /// Deletes a master identity and the identity record under it Future delete() async { final pool = DHTRecordPool.instance; - await (await pool.openRead(masterRecordKey)).delete(); + await pool.delete(masterRecordKey); } Future get identityCrypto => diff --git a/packages/veilid_support/pubspec.lock b/packages/veilid_support/pubspec.lock index a6ec0d8..352be05 100644 --- a/packages/veilid_support/pubspec.lock +++ b/packages/veilid_support/pubspec.lock @@ -203,10 +203,10 @@ packages: dependency: transitive description: name: dart_style - sha256: "40ae61a5d43feea6d24bd22c0537a6629db858963b99b4bc1c3db80676f32368" + sha256: "99e066ce75c89d6b29903d788a7bb9369cf754f7b24bf70bf4b6d6d6b26853b9" url: "https://pub.dev" source: hosted - version: "2.3.4" + version: "2.3.6" equatable: dependency: "direct main" description: @@ -219,10 +219,10 @@ packages: dependency: "direct main" description: name: fast_immutable_collections - sha256: "6df5b5bb29f52644c4c653ef0ae7d26c8463f8d6551b0ac94561103ff6c5ca17" + sha256: "49154d1da38a34519b907b0e94a06705a59b7127728131dc4a54fe62fd95a83e" url: "https://pub.dev" source: hosted - version: "10.1.1" + version: "10.2.1" ffi: dependency: transitive description: @@ -728,10 +728,10 @@ packages: dependency: transitive description: name: vm_service - sha256: e7d5ecd604e499358c5fe35ee828c0298a320d54455e791e9dcf73486bc8d9f0 + sha256: a75f83f14ad81d5fe4b3319710b90dec37da0e22612326b696c9e1b8f34bbf48 url: "https://pub.dev" source: hosted - version: "14.1.0" + version: "14.2.0" watcher: dependency: transitive description: @@ -744,10 +744,10 @@ packages: dependency: transitive description: name: web - sha256: "1d9158c616048c38f712a6646e317a3426da10e884447626167240d45209cbad" + sha256: "97da13628db363c635202ad97068d47c5b8aa555808e7a9411963c533b449b27" url: "https://pub.dev" source: hosted - version: "0.5.0" + version: "0.5.1" web_socket_channel: dependency: transitive description: @@ -768,10 +768,10 @@ packages: dependency: transitive description: name: win32 - sha256: "464f5674532865248444b4c3daca12bd9bf2d7c47f759ce2617986e7229494a8" + sha256: "8cb58b45c47dcb42ab3651533626161d6b67a2921917d8d429791f76972b3480" url: "https://pub.dev" source: hosted - version: "5.2.0" + version: "5.3.0" xdg_directories: dependency: transitive description: @@ -790,4 +790,4 @@ packages: version: "3.1.2" sdks: dart: ">=3.3.0 <4.0.0" - flutter: ">=3.10.6" + flutter: ">=3.19.1" diff --git a/pubspec.lock b/pubspec.lock index c1c3af8..9a49fa3 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -68,10 +68,10 @@ packages: dependency: "direct main" description: name: awesome_extensions - sha256: cde9c8c155c1a1cafc5286807e16124e97f0cff739a47ec17aa9d26c3c37abcf + sha256: "7d235d64a81543a7e200a91b1149bef7d32241290fa483bae25b31be41449a7c" url: "https://pub.dev" source: hosted - version: "2.0.12" + version: "2.0.13" badges: dependency: "direct main" description: @@ -219,18 +219,18 @@ packages: dependency: transitive description: name: camera_android - sha256: "351429510121d179b9aac5a2e8cb525c3cd6c39f4d709c5f72dfb21726e52371" + sha256: "15a6543878a41c141807ffab496f66b7fef6da0f23372f5513fc6349e60f437e" url: "https://pub.dev" source: hosted - version: "0.10.8+16" + version: "0.10.8+17" camera_avfoundation: dependency: transitive description: name: camera_avfoundation - sha256: "7d0763dfcbf060f56aa254a68c103210280bee9e97bbe4fdef23e257a4f70ab9" + sha256: "8b113e43ee4434c9244c03c905432a0d5956cedaded3cd7381abaab89ce50297" url: "https://pub.dev" source: hosted - version: "0.9.14" + version: "0.9.14+1" camera_platform_interface: dependency: transitive description: @@ -379,10 +379,10 @@ packages: dependency: transitive description: name: dart_style - sha256: "40ae61a5d43feea6d24bd22c0537a6629db858963b99b4bc1c3db80676f32368" + sha256: "99e066ce75c89d6b29903d788a7bb9369cf754f7b24bf70bf4b6d6d6b26853b9" url: "https://pub.dev" source: hosted - version: "2.3.4" + version: "2.3.6" diffutil_dart: dependency: transitive description: @@ -403,10 +403,10 @@ packages: dependency: "direct main" description: name: fast_immutable_collections - sha256: "6df5b5bb29f52644c4c653ef0ae7d26c8463f8d6551b0ac94561103ff6c5ca17" + sha256: "49154d1da38a34519b907b0e94a06705a59b7127728131dc4a54fe62fd95a83e" url: "https://pub.dev" source: hosted - version: "10.1.1" + version: "10.2.1" ffi: dependency: transitive description: @@ -517,10 +517,10 @@ packages: dependency: "direct main" description: name: flutter_native_splash - sha256: "558f10070f03ee71f850a78f7136ab239a67636a294a44a06b6b7345178edb1e" + sha256: edf39bcf4d74aca1eb2c1e43c3e445fd9f494013df7f0da752fefe72020eedc0 url: "https://pub.dev" source: hosted - version: "2.3.10" + version: "2.4.0" flutter_parsed_text: dependency: transitive description: @@ -549,10 +549,10 @@ packages: dependency: "direct main" description: name: flutter_slidable - sha256: "19ed4813003a6ff4e9c6bcce37e792a2a358919d7603b2b31ff200229191e44c" + sha256: "673403d2eeef1f9e8483bd6d8d92aae73b1d8bd71f382bc3930f699c731bc27c" url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "3.1.0" flutter_spinkit: dependency: "direct main" description: @@ -634,10 +634,10 @@ packages: dependency: "direct main" description: name: go_router - sha256: "170c46e237d6eb0e6e9f0e8b3f56101e14fb64f787016e42edd74c39cf8b176a" + sha256: "7ecb2f391edbca5473db591b48555a8912dde60edd0fb3013bd6743033b2d3f8" url: "https://pub.dev" source: hosted - version: "13.2.0" + version: "13.2.1" graphs: dependency: transitive description: @@ -818,10 +818,10 @@ packages: dependency: "direct main" description: name: mobile_scanner - sha256: "619ed5fd43ca9007a151f00c3dc43feedeaf235fe5647735d0237c38849d49dc" + sha256: "827765afbd4792ff3fd105ad593821ac0f6d8a7d352689013b07ee85be336312" url: "https://pub.dev" source: hosted - version: "4.0.0" + version: "4.0.1" motion_toast: dependency: "direct main" description: @@ -1009,10 +1009,10 @@ packages: dependency: "direct main" description: name: provider - sha256: "9a96a0a19b594dbc5bf0f1f27d2bc67d5f95957359b461cd9feb44ed6ae75096" + sha256: c8a055ee5ce3fd98d6fc872478b03823ffdb448699c6ebdbbc71d59b596fd48c url: "https://pub.dev" source: hosted - version: "6.1.1" + version: "6.1.2" pub_semver: dependency: transitive description: @@ -1041,10 +1041,10 @@ packages: dependency: "direct main" description: name: qr_code_dart_scan - sha256: b42d097e346a546fcf9ff2f5a0e39ea1315449608cfd9b2bc6513988b488a371 + sha256: "8e9732d5b6e4e28d50647dc6d7713bf421148cadf28c768a10e9810bf6f3d87a" url: "https://pub.dev" source: hosted - version: "0.7.5" + version: "0.7.6" qr_flutter: dependency: "direct main" description: @@ -1057,10 +1057,10 @@ packages: dependency: "direct main" description: name: quickalert - sha256: "0c21c9be68b9ae76082e1ad56db9f51202a38e617e08376f05375238277cfb5a" + sha256: b5d62b1e20b08cc0ff5f40b6da519bdc7a5de6082f13d90572cf4e72eea56c5e url: "https://pub.dev" source: hosted - version: "1.0.2" + version: "1.1.0" quiver: dependency: transitive description: @@ -1113,10 +1113,10 @@ packages: dependency: "direct main" description: name: searchable_listview - sha256: "5cd3cd87e0cbd4e6685f6798a9bb4bcc170df20fb92beb662b978f5fccded634" + sha256: "5535ea3efa4599cf23ce52870a9580b52ece5d691aa90655ebec76d5081c9592" url: "https://pub.dev" source: hosted - version: "2.10.2" + version: "2.11.1" share_plus: dependency: "direct main" description: @@ -1129,10 +1129,10 @@ packages: dependency: transitive description: name: share_plus_platform_interface - sha256: df08bc3a07d01f5ea47b45d03ffcba1fa9cd5370fb44b3f38c70e42cced0f956 + sha256: "251eb156a8b5fa9ce033747d73535bf53911071f8d3b6f4f0b578505ce0d4496" url: "https://pub.dev" source: hosted - version: "3.3.1" + version: "3.4.0" shared_preferences: dependency: "direct main" description: @@ -1278,10 +1278,10 @@ packages: dependency: transitive description: name: sqflite_common - sha256: "28d8c66baee4968519fb8bd6cdbedad982d6e53359091f0b74544a9f32ec72d5" + sha256: "3da423ce7baf868be70e2c0976c28a1bb2f73644268b7ffa7d2e08eab71f16a4" url: "https://pub.dev" source: hosted - version: "2.5.3" + version: "2.5.4" stack_trace: dependency: "direct main" description: @@ -1532,10 +1532,10 @@ packages: dependency: transitive description: name: web - sha256: "1d9158c616048c38f712a6646e317a3426da10e884447626167240d45209cbad" + sha256: "97da13628db363c635202ad97068d47c5b8aa555808e7a9411963c533b449b27" url: "https://pub.dev" source: hosted - version: "0.5.0" + version: "0.5.1" web_socket_channel: dependency: transitive description: @@ -1548,10 +1548,10 @@ packages: dependency: transitive description: name: win32 - sha256: "464f5674532865248444b4c3daca12bd9bf2d7c47f759ce2617986e7229494a8" + sha256: "8cb58b45c47dcb42ab3651533626161d6b67a2921917d8d429791f76972b3480" url: "https://pub.dev" source: hosted - version: "5.2.0" + version: "5.3.0" window_manager: dependency: "direct main" description: From 7b6430798735e854886cc0a6da3b10b0dbb804e3 Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Fri, 29 Mar 2024 20:28:56 -0400 Subject: [PATCH 64/68] more shortarray work --- .../src/dht_short_array/dht_short_array.dart | 282 +----------------- .../dht_short_array/dht_short_array_head.dart | 6 +- 2 files changed, 19 insertions(+), 269 deletions(-) diff --git a/packages/veilid_support/lib/dht_support/src/dht_short_array/dht_short_array.dart b/packages/veilid_support/lib/dht_support/src/dht_short_array/dht_short_array.dart index 2fca99e..fe8b705 100644 --- a/packages/veilid_support/lib/dht_support/src/dht_short_array/dht_short_array.dart +++ b/packages/veilid_support/lib/dht_support/src/dht_short_array/dht_short_array.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'dart:typed_data'; +import 'package:async_tools/async_tools.dart'; import 'package:mutex/mutex.dart'; import 'package:protobuf/protobuf.dart'; @@ -8,6 +9,8 @@ import '../../../veilid_support.dart'; import '../../proto/proto.dart' as proto; part 'dht_short_array_head.dart'; +part 'dht_short_array_read.dart'; +part 'dht_short_array_write.dart'; /////////////////////////////////////////////////////////////////////// @@ -128,9 +131,6 @@ class DHTShortArray { /// Get the record pointer foir this shortarray OwnedDHTRecordPointer get recordPointer => _head.recordPointer; - /// Returns the number of elements in the DHTShortArray - int get length => _head.length; - /// Free all resources for the DHTShortArray Future close() async { await _watchController?.close(); @@ -168,250 +168,22 @@ class DHTShortArray { } } - /// Return the item at position 'pos' in the DHTShortArray. If 'forceRefresh' - /// is specified, the network will always be checked for newer values - /// rather than returning the existing locally stored copy of the elements. - Future getItem(int pos, {bool forceRefresh = false}) async => - _head.operate( - (head) async => _getItemInner(head, pos, forceRefresh: forceRefresh)); - - Future _getItemInner(_DHTShortArrayHead head, int pos, - {bool forceRefresh = false}) async { - if (pos < 0 || pos >= length) { - throw IndexError.withLength(pos, length); - } - - final (record, recordSubkey) = await head.lookupPosition(pos); - - final refresh = forceRefresh || head.positionNeedsRefresh(pos); - final out = record.get(subkey: recordSubkey, forceRefresh: refresh); - await head.updatePositionSeq(pos, false); - - return out; - } - - /// Return a list of all of the items in the DHTShortArray. If 'forceRefresh' - /// is specified, the network will always be checked for newer values - /// rather than returning the existing locally stored copy of the elements. - Future?> getAllItems({bool forceRefresh = false}) async => + /// Runs a closure allowing read-only access to the shortarray + Future operate(Future Function(DHTShortArrayRead) closure) async => _head.operate((head) async { - final out = []; - - for (var pos = 0; pos < head.length; pos++) { - final elem = - await _getItemInner(head, pos, forceRefresh: forceRefresh); - if (elem == null) { - return null; - } - out.add(elem); - } - - return out; + final reader = _DHTShortArrayRead._(head); + return closure(reader); }); - /// Convenience function: - /// Like getItem but also parses the returned element as JSON - Future getItemJson(T Function(dynamic) fromJson, int pos, - {bool forceRefresh = false}) => - getItem(pos, forceRefresh: forceRefresh) - .then((out) => jsonDecodeOptBytes(fromJson, out)); - - /// Convenience function: - /// Like getAllItems but also parses the returned elements as JSON - Future?> getAllItemsJson(T Function(dynamic) fromJson, - {bool forceRefresh = false}) => - getAllItems(forceRefresh: forceRefresh) - .then((out) => out?.map(fromJson).toList()); - - /// Convenience function: - /// Like getItem but also parses the returned element as a protobuf object - Future getItemProtobuf( - T Function(List) fromBuffer, int pos, - {bool forceRefresh = false}) => - getItem(pos, forceRefresh: forceRefresh) - .then((out) => (out == null) ? null : fromBuffer(out)); - - /// Convenience function: - /// Like getAllItems but also parses the returned elements as protobuf objects - Future?> getAllItemsProtobuf( - T Function(List) fromBuffer, - {bool forceRefresh = false}) => - getAllItems(forceRefresh: forceRefresh) - .then((out) => out?.map(fromBuffer).toList()); - - /// Try to add an item to the end of the DHTShortArray. Return true if the - /// element was successfully added, and false if the state changed before - /// the element could be added or a newer value was found on the network. - /// This may throw an exception if the number elements added exceeds the - /// built-in limit of 'maxElements = 256' entries. - Future tryAddItem(Uint8List value) async { - final out = await _head - .operateWrite((head) async => _tryAddItemInner(head, value)) ?? - false; - return out; - } - - Future _tryAddItemInner( - _DHTShortArrayHead head, Uint8List value) async { - // Allocate empty index at the end of the list - final pos = head.length; - head.allocateIndex(pos); - - // Write item - final (_, wasSet) = await _tryWriteItemInner(head, pos, value); - if (!wasSet) { - return false; - } - - // Get sequence number written - await head.updatePositionSeq(pos, true); - - return true; - } - - /// Try to insert an item as position 'pos' of the DHTShortArray. - /// Return true if the element was successfully inserted, and false if the - /// state changed before the element could be inserted or a newer value was - /// found on the network. - /// This may throw an exception if the number elements added exceeds the - /// built-in limit of 'maxElements = 256' entries. - Future tryInsertItem(int pos, Uint8List value) async { - final out = await _head.operateWrite( - (head) async => _tryInsertItemInner(head, pos, value)) ?? - false; - - return out; - } - - Future _tryInsertItemInner( - _DHTShortArrayHead head, int pos, Uint8List value) async { - // Allocate empty index at position - head.allocateIndex(pos); - - // Write item - final (_, wasSet) = await _tryWriteItemInner(head, pos, value); - if (!wasSet) { - return false; - } - - // Get sequence number written - await head.updatePositionSeq(pos, true); - - return true; - } - - /// Try to swap items at position 'aPos' and 'bPos' in the DHTShortArray. - /// Return true if the elements were successfully swapped, and false if the - /// state changed before the elements could be swapped or newer values were - /// found on the network. - /// This may throw an exception if either of the positions swapped exceed - /// the length of the list - - Future trySwapItem(int aPos, int bPos) async { - final out = await _head.operateWrite( - (head) async => _trySwapItemInner(head, aPos, bPos)) ?? - false; - - return out; - } - - Future _trySwapItemInner( - _DHTShortArrayHead head, int aPos, int bPos) async { - // Swap indices - head.swapIndex(aPos, bPos); - - return true; - } - - /// Try to remove an item at position 'pos' in the DHTShortArray. - /// Return the element if it was successfully removed, and null if the - /// state changed before the elements could be removed or newer values were - /// found on the network. - /// This may throw an exception if the position removed exceeeds the length of - /// the list. - - Future tryRemoveItem(int pos) async { - final out = - _head.operateWrite((head) async => _tryRemoveItemInner(head, pos)); - - return out; - } - - Future _tryRemoveItemInner( - _DHTShortArrayHead head, int pos) async { - final (record, recordSubkey) = await head.lookupPosition(pos); - final result = await record.get(subkey: recordSubkey); - if (result == null) { - throw StateError('Element does not exist'); - } - head.freeIndex(pos); - return result; - } - - /// Convenience function: - /// Like removeItem but also parses the returned element as JSON - Future tryRemoveItemJson( - T Function(dynamic) fromJson, - int pos, - ) => - tryRemoveItem(pos).then((out) => jsonDecodeOptBytes(fromJson, out)); - - /// Convenience function: - /// Like removeItem but also parses the returned element as JSON - Future tryRemoveItemProtobuf( - T Function(List) fromBuffer, int pos) => - getItem(pos).then((out) => (out == null) ? null : fromBuffer(out)); - - /// Try to remove all items in the DHTShortArray. - /// Return true if it was successfully cleared, and false if the - /// state changed before the elements could be cleared or newer values were - /// found on the network. - Future tryClear() async { - final out = - await _head.operateWrite((head) async => _tryClearInner(head)) ?? false; - - return out; - } - - Future _tryClearInner(_DHTShortArrayHead head) async { - head.clearIndex(); - return true; - } - - /// Try to set an item at position 'pos' of the DHTShortArray. - /// If the set was successful this returns: - /// * The prior contents of the element, or null if there was no value yet - /// * A boolean true - /// If the set was found a newer value on the network: - /// * The newer value of the element, or null if the head record - /// changed. - /// * A boolean false - /// This may throw an exception if the position exceeds the built-in limit of - /// 'maxElements = 256' entries. - Future<(Uint8List?, bool)> tryWriteItem(int pos, Uint8List newValue) async { - final out = await _head - .operateWrite((head) async => _tryWriteItemInner(head, pos, newValue)); - if (out == null) { - return (null, false); - } - - return out; - } - - Future<(Uint8List?, bool)> _tryWriteItemInner( - _DHTShortArrayHead head, int pos, Uint8List newValue) async { - if (pos < 0 || pos >= head.length) { - throw IndexError.withLength(pos, head.length); - } - final (record, recordSubkey) = await head.lookupPosition(pos); - final oldValue = await record.get(subkey: recordSubkey); - final result = await record.tryWriteBytes(newValue, subkey: recordSubkey); - if (result != null) { - // A result coming back means the element was overwritten already - return (result, false); - } - return (oldValue, true); - } + /// Runs a closure allowing read-write access to the shortarray + /// Returns (result, true) of the closure if the write could be performed + /// Returns (null, false) if the write could not be performed at this time + Future<(T?, bool)> operateWrite( + Future Function(DHTShortArrayWrite) closure) async => + _head.operateWrite((head) async { + final writer = _DHTShortArrayWrite._(head); + return closure(writer); + }); /// Set an item at position 'pos' of the DHTShortArray. Retries until the /// value being written is successfully made the newest value of the element. @@ -449,28 +221,6 @@ class DHTShortArray { }, timeout: timeout); } - /// Convenience function: - /// Like tryWriteItem but also encodes the input value as JSON and parses the - /// returned element as JSON - Future<(T?, bool)> tryWriteItemJson( - T Function(dynamic) fromJson, - int pos, - T newValue, - ) => - tryWriteItem(pos, jsonEncodeBytes(newValue)) - .then((out) => (jsonDecodeOptBytes(fromJson, out.$1), out.$2)); - - /// Convenience function: - /// Like tryWriteItem but also encodes the input value as a protobuf object - /// and parses the returned element as a protobuf object - Future<(T?, bool)> tryWriteItemProtobuf( - T Function(List) fromBuffer, - int pos, - T newValue, - ) => - tryWriteItem(pos, newValue.writeToBuffer()).then( - (out) => ((out.$1 == null ? null : fromBuffer(out.$1!)), out.$2)); - /// Convenience function: /// Like eventualWriteItem but also encodes the input value as JSON and parses /// the returned element as JSON diff --git a/packages/veilid_support/lib/dht_support/src/dht_short_array/dht_short_array_head.dart b/packages/veilid_support/lib/dht_support/src/dht_short_array/dht_short_array_head.dart index 708bb38..bd06663 100644 --- a/packages/veilid_support/lib/dht_support/src/dht_short_array/dht_short_array_head.dart +++ b/packages/veilid_support/lib/dht_support/src/dht_short_array/dht_short_array_head.dart @@ -57,7 +57,7 @@ class _DHTShortArrayHead { return closure(this); }); - Future operateWrite( + Future<(T?, bool)> operateWrite( Future Function(_DHTShortArrayHead) closure) async => _headMutex.protect(() async { final oldLinkedRecords = List.of(_linkedRecords); @@ -70,11 +70,11 @@ class _DHTShortArrayHead { if (!await _writeHead()) { // Failed to write head means head got overwritten so write should // be considered failed - return null; + return (null, false); } onUpdatedHead?.call(); - return out; + return (out, true); } on Exception { // Exception means state needs to be reverted _linkedRecords = oldLinkedRecords; From 13dfc5eaa0c4200a6f9a5949faafa2f4cb0c4e45 Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Fri, 29 Mar 2024 21:48:01 -0400 Subject: [PATCH 65/68] add missing files --- .../dht_short_array/dht_short_array_read.dart | 101 +++++++++ .../dht_short_array_write.dart | 195 ++++++++++++++++++ 2 files changed, 296 insertions(+) create mode 100644 packages/veilid_support/lib/dht_support/src/dht_short_array/dht_short_array_read.dart create mode 100644 packages/veilid_support/lib/dht_support/src/dht_short_array/dht_short_array_write.dart diff --git a/packages/veilid_support/lib/dht_support/src/dht_short_array/dht_short_array_read.dart b/packages/veilid_support/lib/dht_support/src/dht_short_array/dht_short_array_read.dart new file mode 100644 index 0000000..fccdf20 --- /dev/null +++ b/packages/veilid_support/lib/dht_support/src/dht_short_array/dht_short_array_read.dart @@ -0,0 +1,101 @@ +part of 'dht_short_array.dart'; + +//////////////////////////////////////////////////////////////////////////// +// Reader interface +abstract class DHTShortArrayRead { + /// Returns the number of elements in the DHTShortArray + int get length; + + /// Return the item at position 'pos' in the DHTShortArray. If 'forceRefresh' + /// is specified, the network will always be checked for newer values + /// rather than returning the existing locally stored copy of the elements. + Future getItem(int pos, {bool forceRefresh = false}); + + /// Return a list of all of the items in the DHTShortArray. If 'forceRefresh' + /// is specified, the network will always be checked for newer values + /// rather than returning the existing locally stored copy of the elements. + Future?> getAllItems({bool forceRefresh = false}); +} + +extension DHTShortArrayReadExt on DHTShortArrayRead { + /// Convenience function: + /// Like getItem but also parses the returned element as JSON + Future getItemJson(T Function(dynamic) fromJson, int pos, + {bool forceRefresh = false}) => + getItem(pos, forceRefresh: forceRefresh) + .then((out) => jsonDecodeOptBytes(fromJson, out)); + + /// Convenience function: + /// Like getAllItems but also parses the returned elements as JSON + Future?> getAllItemsJson(T Function(dynamic) fromJson, + {bool forceRefresh = false}) => + getAllItems(forceRefresh: forceRefresh) + .then((out) => out?.map(fromJson).toList()); + + /// Convenience function: + /// Like getItem but also parses the returned element as a protobuf object + Future getItemProtobuf( + T Function(List) fromBuffer, int pos, + {bool forceRefresh = false}) => + getItem(pos, forceRefresh: forceRefresh) + .then((out) => (out == null) ? null : fromBuffer(out)); + + /// Convenience function: + /// Like getAllItems but also parses the returned elements as protobuf objects + Future?> getAllItemsProtobuf( + T Function(List) fromBuffer, + {bool forceRefresh = false}) => + getAllItems(forceRefresh: forceRefresh) + .then((out) => out?.map(fromBuffer).toList()); +} + +//////////////////////////////////////////////////////////////////////////// +// Reader-only implementation + +class _DHTShortArrayRead implements DHTShortArrayRead { + _DHTShortArrayRead._(_DHTShortArrayHead head) : _head = head; + + /// Returns the number of elements in the DHTShortArray + @override + int get length => _head.length; + + /// Return the item at position 'pos' in the DHTShortArray. If 'forceRefresh' + /// is specified, the network will always be checked for newer values + /// rather than returning the existing locally stored copy of the elements. + @override + Future getItem(int pos, {bool forceRefresh = false}) async { + if (pos < 0 || pos >= length) { + throw IndexError.withLength(pos, length); + } + + final (record, recordSubkey) = await _head.lookupPosition(pos); + + final refresh = forceRefresh || _head.positionNeedsRefresh(pos); + final out = record.get(subkey: recordSubkey, forceRefresh: refresh); + await _head.updatePositionSeq(pos, false); + + return out; + } + + /// Return a list of all of the items in the DHTShortArray. If 'forceRefresh' + /// is specified, the network will always be checked for newer values + /// rather than returning the existing locally stored copy of the elements. + @override + Future?> getAllItems({bool forceRefresh = false}) async { + final out = []; + + for (var pos = 0; pos < _head.length; pos++) { + final elem = await getItem(pos, forceRefresh: forceRefresh); + if (elem == null) { + return null; + } + out.add(elem); + } + + return out; + } + + //////////////////////////////////////////////////////////////////////////// + // Fields + final _DHTShortArrayHead _head; +} diff --git a/packages/veilid_support/lib/dht_support/src/dht_short_array/dht_short_array_write.dart b/packages/veilid_support/lib/dht_support/src/dht_short_array/dht_short_array_write.dart new file mode 100644 index 0000000..76e9ce2 --- /dev/null +++ b/packages/veilid_support/lib/dht_support/src/dht_short_array/dht_short_array_write.dart @@ -0,0 +1,195 @@ +part of 'dht_short_array.dart'; + +//////////////////////////////////////////////////////////////////////////// +// Writer interface +abstract class DHTShortArrayWrite implements DHTShortArrayRead { + /// Try to add an item to the end of the DHTShortArray. Return true if the + /// element was successfully added, and false if the state changed before + /// the element could be added or a newer value was found on the network. + /// This may throw an exception if the number elements added exceeds the + /// built-in limit of 'maxElements = 256' entries. + Future tryAddItem(Uint8List value); + + /// Try to insert an item as position 'pos' of the DHTShortArray. + /// Return true if the element was successfully inserted, and false if the + /// state changed before the element could be inserted or a newer value was + /// found on the network. + /// This may throw an exception if the number elements added exceeds the + /// built-in limit of 'maxElements = 256' entries. + Future tryInsertItem(int pos, Uint8List value); + + /// Try to swap items at position 'aPos' and 'bPos' in the DHTShortArray. + /// Return true if the elements were successfully swapped, and false if the + /// state changed before the elements could be swapped or newer values were + /// found on the network. + /// This may throw an exception if either of the positions swapped exceed + /// the length of the list + Future trySwapItem(int aPos, int bPos); + + /// Try to remove an item at position 'pos' in the DHTShortArray. + /// Return the element if it was successfully removed, and null if the + /// state changed before the elements could be removed or newer values were + /// found on the network. + /// This may throw an exception if the position removed exceeeds the length of + /// the list. + Future tryRemoveItem(int pos); + + /// Try to remove all items in the DHTShortArray. + /// Return true if it was successfully cleared, and false if the + /// state changed before the elements could be cleared or newer values were + /// found on the network. + Future tryClear(); + + /// Try to set an item at position 'pos' of the DHTShortArray. + /// If the set was successful this returns: + /// * The prior contents of the element, or null if there was no value yet + /// * A boolean true + /// If the set was found a newer value on the network: + /// * The newer value of the element, or null if the head record + /// changed. + /// * A boolean false + /// This may throw an exception if the position exceeds the built-in limit of + /// 'maxElements = 256' entries. + Future<(Uint8List?, bool)> tryWriteItem(int pos, Uint8List newValue); +} + +extension DHTShortArrayWriteExt on DHTShortArrayWrite { + /// Convenience function: + /// Like removeItem but also parses the returned element as JSON + Future tryRemoveItemJson( + T Function(dynamic) fromJson, + int pos, + ) => + tryRemoveItem(pos).then((out) => jsonDecodeOptBytes(fromJson, out)); + + /// Convenience function: + /// Like removeItem but also parses the returned element as JSON + Future tryRemoveItemProtobuf( + T Function(List) fromBuffer, int pos) => + getItem(pos).then((out) => (out == null) ? null : fromBuffer(out)); + + /// Convenience function: + /// Like tryWriteItem but also encodes the input value as JSON and parses the + /// returned element as JSON + Future<(T?, bool)> tryWriteItemJson( + T Function(dynamic) fromJson, + int pos, + T newValue, + ) => + tryWriteItem(pos, jsonEncodeBytes(newValue)) + .then((out) => (jsonDecodeOptBytes(fromJson, out.$1), out.$2)); + + /// Convenience function: + /// Like tryWriteItem but also encodes the input value as a protobuf object + /// and parses the returned element as a protobuf object + Future<(T?, bool)> tryWriteItemProtobuf( + T Function(List) fromBuffer, + int pos, + T newValue, + ) => + tryWriteItem(pos, newValue.writeToBuffer()).then( + (out) => ((out.$1 == null ? null : fromBuffer(out.$1!)), out.$2)); +} + +//////////////////////////////////////////////////////////////////////////// +// Writer-only implementation + +class _DHTShortArrayWrite implements DHTShortArrayWrite { + _DHTShortArrayWrite._(_DHTShortArrayHead head) + : _head = head, + _reader = _DHTShortArrayRead._(head); + + @override + Future tryAddItem(Uint8List value) async { + // Allocate empty index at the end of the list + final pos = _head.length; + _head.allocateIndex(pos); + + // Write item + final (_, wasSet) = await tryWriteItem(pos, value); + if (!wasSet) { + return false; + } + + // Get sequence number written + await _head.updatePositionSeq(pos, true); + + return true; + } + + @override + Future tryInsertItem(int pos, Uint8List value) async { + // Allocate empty index at position + _head.allocateIndex(pos); + + // Write item + final (_, wasSet) = await tryWriteItem(pos, value); + if (!wasSet) { + return false; + } + + // Get sequence number written + await _head.updatePositionSeq(pos, true); + + return true; + } + + @override + Future trySwapItem(int aPos, int bPos) async { + // Swap indices + _head.swapIndex(aPos, bPos); + + return true; + } + + @override + Future tryRemoveItem(int pos) async { + final (record, recordSubkey) = await _head.lookupPosition(pos); + final result = await record.get(subkey: recordSubkey); + if (result == null) { + throw StateError('Element does not exist'); + } + _head.freeIndex(pos); + return result; + } + + @override + Future tryClear() async { + _head.clearIndex(); + return true; + } + + @override + Future<(Uint8List?, bool)> tryWriteItem(int pos, Uint8List newValue) async { + if (pos < 0 || pos >= _head.length) { + throw IndexError.withLength(pos, _head.length); + } + final (record, recordSubkey) = await _head.lookupPosition(pos); + final oldValue = await record.get(subkey: recordSubkey); + final result = await record.tryWriteBytes(newValue, subkey: recordSubkey); + if (result != null) { + // A result coming back means the element was overwritten already + return (result, false); + } + return (oldValue, true); + } + + //////////////////////////////////////////////////////////////////////////// + // Reader passthrough + + @override + int get length => _reader.length; + + @override + Future getItem(int pos, {bool forceRefresh = false}) => + _reader.getItem(pos, forceRefresh: forceRefresh); + + @override + Future?> getAllItems({bool forceRefresh = false}) => + _reader.getAllItems(forceRefresh: forceRefresh); + + //////////////////////////////////////////////////////////////////////////// + // Fields + final _DHTShortArrayHead _head; + final _DHTShortArrayRead _reader; +} From c48305cba119851bd37fef8c0922f447b22069bc Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Fri, 29 Mar 2024 21:50:27 -0400 Subject: [PATCH 66/68] pub upgrade --- packages/veilid_support/pubspec.lock | 18 +++++++++---- pubspec.lock | 38 ++++++++++++++-------------- pubspec.yaml | 32 +++++++++++------------ 3 files changed, 48 insertions(+), 40 deletions(-) diff --git a/packages/veilid_support/pubspec.lock b/packages/veilid_support/pubspec.lock index 352be05..7e13fbb 100644 --- a/packages/veilid_support/pubspec.lock +++ b/packages/veilid_support/pubspec.lock @@ -131,10 +131,10 @@ packages: dependency: transitive description: name: change_case - sha256: "47c48c36f95f20c6d0ba03efabceff261d05026cca322cc2c4c01c343371b5bb" + sha256: f4e08feaa845e75e4f5ad2b0e15f24813d7ea6c27e7b78252f0c17f752cf1157 url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "1.1.0" characters: dependency: transitive description: @@ -239,6 +239,14 @@ packages: url: "https://pub.dev" source: hosted version: "7.0.0" + file_utils: + dependency: transitive + description: + name: file_utils + sha256: d1e64389a22649095c8405c9e177272caf05139255931c9ff30d53b5c9bcaa34 + url: "https://pub.dev" + source: hosted + version: "1.0.1" fixnum: dependency: transitive description: @@ -649,10 +657,10 @@ packages: dependency: transitive description: name: system_info2 - sha256: "65206bbef475217008b5827374767550a5420ce70a04d2d7e94d1d2253f3efc9" + sha256: af2f948e3f31a3367a049932a8ad59faf0063ecf836a020d975b9f41566d8bc9 url: "https://pub.dev" source: hosted - version: "4.0.0" + version: "3.0.2" system_info_plus: dependency: transitive description: @@ -790,4 +798,4 @@ packages: version: "3.1.2" sdks: dart: ">=3.3.0 <4.0.0" - flutter: ">=3.19.1" + flutter: ">=3.10.6" diff --git a/pubspec.lock b/pubspec.lock index 9a49fa3..c2cf50b 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -92,10 +92,10 @@ packages: dependency: "direct main" description: name: bloc - sha256: f53a110e3b48dcd78136c10daa5d51512443cea5e1348c9d80a320095fa2db9e + sha256: "106842ad6569f0b60297619e9e0b1885c2fb9bf84812935490e6c5275777804e" url: "https://pub.dev" source: hosted - version: "8.1.3" + version: "8.1.4" bloc_tools: dependency: "direct main" description: @@ -155,10 +155,10 @@ packages: dependency: "direct dev" description: name: build_runner - sha256: "581bacf68f89ec8792f5e5a0b2c4decd1c948e97ce659dc783688c8a88fbec21" + sha256: "3ac61a79bfb6f6cc11f693591063a7f19a7af628dc52f141743edac5c16e8c22" url: "https://pub.dev" source: hosted - version: "2.4.8" + version: "2.4.9" build_runner_core: dependency: transitive description: @@ -219,10 +219,10 @@ packages: dependency: transitive description: name: camera_android - sha256: "15a6543878a41c141807ffab496f66b7fef6da0f23372f5513fc6349e60f437e" + sha256: "1100e527b44a96906987a91ef78c8dacb539e34612a8058de89023380acf67f1" url: "https://pub.dev" source: hosted - version: "0.10.8+17" + version: "0.10.8+18" camera_avfoundation: dependency: transitive description: @@ -448,10 +448,10 @@ packages: dependency: "direct main" description: name: flutter_bloc - sha256: "87325da1ac757fcc4813e6b34ed5dd61169973871fdf181d6c2109dd6935ece1" + sha256: f0ecf6e6eb955193ca60af2d5ca39565a86b8a142452c5b24d96fb477428f4d2 url: "https://pub.dev" source: hosted - version: "8.1.4" + version: "8.1.5" flutter_cache_manager: dependency: transitive description: @@ -557,10 +557,10 @@ packages: dependency: "direct main" description: name: flutter_spinkit - sha256: b39c753e909d4796906c5696a14daf33639a76e017136c8d82bf3e620ce5bb8e + sha256: d2696eed13732831414595b98863260e33e8882fc069ee80ec35d4ac9ddb0472 url: "https://pub.dev" source: hosted - version: "5.2.0" + version: "5.2.1" flutter_svg: dependency: "direct main" description: @@ -610,10 +610,10 @@ packages: dependency: transitive description: name: frontend_server_client - sha256: "408e3ca148b31c20282ad6f37ebfa6f4bdc8fede5b74bc2f08d9d92b55db3612" + sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694 url: "https://pub.dev" source: hosted - version: "3.2.0" + version: "4.0.0" glob: dependency: transitive description: @@ -634,10 +634,10 @@ packages: dependency: "direct main" description: name: go_router - sha256: "7ecb2f391edbca5473db591b48555a8912dde60edd0fb3013bd6743033b2d3f8" + sha256: "5ed2687bc961f33a752017ccaa7edead3e5601b28b6376a5901bf24728556b85" url: "https://pub.dev" source: hosted - version: "13.2.1" + version: "13.2.2" graphs: dependency: transitive description: @@ -690,10 +690,10 @@ packages: dependency: "direct main" description: name: hydrated_bloc - sha256: "00a2099680162e74b5a836b8a7f446e478520a9cae9f6032e028ad8129f4432d" + sha256: af35b357739fe41728df10bec03aad422cdc725a1e702e03af9d2a41ea05160c url: "https://pub.dev" source: hosted - version: "9.1.4" + version: "9.1.5" icons_launcher: dependency: "direct dev" description: @@ -1121,10 +1121,10 @@ packages: dependency: "direct main" description: name: share_plus - sha256: "3ef39599b00059db0990ca2e30fca0a29d8b37aae924d60063f8e0184cf20900" + sha256: "05ec043470319bfbabe0adbc90d3a84cbff0426b9d9f3a6e2ad3e131fa5fa629" url: "https://pub.dev" source: hosted - version: "7.2.2" + version: "8.0.2" share_plus_platform_interface: dependency: transitive description: @@ -1504,7 +1504,7 @@ packages: path: "../veilid/veilid-flutter" relative: true source: path - version: "0.2.5" + version: "0.3.0" veilid_support: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 6fa782e..e03d37b 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -13,10 +13,10 @@ dependencies: archive: ^3.4.10 async_tools: path: packages/async_tools - awesome_extensions: ^2.0.12 + awesome_extensions: ^2.0.13 badges: ^3.1.2 basic_utils: ^5.7.0 - bloc: ^8.1.3 + bloc: ^8.1.4 bloc_tools: path: packages/bloc_tools blurry_modal_progress_hud: ^1.1.1 @@ -27,33 +27,33 @@ dependencies: cool_dropdown: ^2.1.0 cupertino_icons: ^1.0.6 equatable: ^2.0.5 - fast_immutable_collections: ^10.1.1 + fast_immutable_collections: ^10.2.1 fixnum: ^1.1.0 flutter: sdk: flutter flutter_animate: ^4.5.0 - flutter_bloc: ^8.1.4 + flutter_bloc: ^8.1.5 flutter_chat_types: ^3.6.2 flutter_chat_ui: ^1.6.12 flutter_form_builder: ^9.2.1 flutter_hooks: ^0.20.5 flutter_localizations: sdk: flutter - flutter_native_splash: ^2.3.10 - flutter_slidable: ^3.0.1 - flutter_spinkit: ^5.2.0 + flutter_native_splash: ^2.4.0 + flutter_slidable: ^3.1.0 + flutter_spinkit: ^5.2.1 flutter_svg: ^2.0.10+1 flutter_translate: ^4.0.4 form_builder_validators: ^9.1.0 freezed_annotation: ^2.4.1 - go_router: ^13.2.0 - hydrated_bloc: ^9.1.4 + go_router: ^13.2.2 + hydrated_bloc: ^9.1.5 image: ^4.1.7 intl: ^0.18.1 json_annotation: ^4.8.1 loggy: ^2.0.3 meta: ^1.11.0 - mobile_scanner: ^4.0.0 + mobile_scanner: ^4.0.1 motion_toast: ^2.8.0 mutex: path: packages/mutex @@ -63,14 +63,14 @@ dependencies: pinput: ^4.0.0 preload_page_view: ^0.2.0 protobuf: ^3.1.0 - provider: ^6.1.1 - qr_code_dart_scan: ^0.7.5 + provider: ^6.1.2 + qr_code_dart_scan: ^0.7.6 qr_flutter: ^4.1.0 - quickalert: ^1.0.2 + quickalert: ^1.1.0 radix_colors: ^1.0.4 reorderable_grid: ^1.0.10 - searchable_listview: ^2.10.2 - share_plus: ^7.2.2 + searchable_listview: ^2.11.1 + share_plus: ^8.0.2 shared_preferences: ^2.2.2 signal_strength_indicator: ^0.4.1 split_view: ^3.2.1 @@ -88,7 +88,7 @@ dependencies: zxing2: ^0.2.3 dev_dependencies: - build_runner: ^2.4.8 + build_runner: ^2.4.9 freezed: ^2.4.7 icons_launcher: ^2.1.7 json_serializable: ^6.7.1 From 48c9c67ab87911b2cd351acf8614fe3f74c995eb Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Mon, 1 Apr 2024 09:04:54 -0400 Subject: [PATCH 67/68] fix head issue and refactor --- .../cubits/account_record_cubit.dart | 4 +- .../models/active_account_info.dart | 4 +- .../account_repository.dart | 70 +---------- .../cubits/single_contact_messages_cubit.dart | 25 ++-- lib/chat_list/cubits/chat_list_cubit.dart | 18 +-- .../cubits/contact_invitation_list_cubit.dart | 14 +-- .../cubits/contact_request_inbox_cubit.dart | 10 +- .../waiting_invitations_bloc_map_cubit.dart | 4 +- lib/contacts/cubits/contact_list_cubit.dart | 15 ++- lib/contacts/cubits/conversation_cubit.dart | 60 +++++----- .../home_account_ready_shell.dart | 113 ++++++++---------- lib/layout/home/home_shell.dart | 4 +- macos/Podfile.lock | 2 +- .../lib/src/single_stateless_processor.dart | 3 +- .../lib/src/async_transformer_cubit.dart | 5 + .../src/dht_record/dht_record.dart | 4 +- .../src/dht_record/dht_record_cubit.dart | 40 +++---- .../src/dht_short_array/dht_short_array.dart | 87 +++----------- .../dht_short_array_cubit.dart | 58 +++++---- .../dht_short_array/dht_short_array_head.dart | 16 ++- 20 files changed, 229 insertions(+), 327 deletions(-) diff --git a/lib/account_manager/cubits/account_record_cubit.dart b/lib/account_manager/cubits/account_record_cubit.dart index b62d37a..60ddd88 100644 --- a/lib/account_manager/cubits/account_record_cubit.dart +++ b/lib/account_manager/cubits/account_record_cubit.dart @@ -6,8 +6,8 @@ import '../../proto/proto.dart' as proto; class AccountRecordCubit extends DefaultDHTRecordCubit { AccountRecordCubit({ - required super.record, - }) : super.value(decodeState: proto.Account.fromBuffer); + required super.open, + }) : super(decodeState: proto.Account.fromBuffer); @override Future close() async { diff --git a/lib/account_manager/models/active_account_info.dart b/lib/account_manager/models/active_account_info.dart index 5b556d5..7a1437b 100644 --- a/lib/account_manager/models/active_account_info.dart +++ b/lib/account_manager/models/active_account_info.dart @@ -11,7 +11,7 @@ class ActiveAccountInfo { const ActiveAccountInfo({ required this.localAccount, required this.userLogin, - required this.accountRecord, + //required this.accountRecord, }); // @@ -41,5 +41,5 @@ class ActiveAccountInfo { // final LocalAccount localAccount; final UserLogin userLogin; - final DHTRecord accountRecord; + //final DHTRecord accountRecord; } diff --git a/lib/account_manager/repository/account_repository/account_repository.dart b/lib/account_manager/repository/account_repository/account_repository.dart index bbc1351..a13b331 100644 --- a/lib/account_manager/repository/account_repository/account_repository.dart +++ b/lib/account_manager/repository/account_repository/account_repository.dart @@ -49,7 +49,6 @@ class AccountRepository { final TableDBValue> _userLogins; final TableDBValue _activeLocalAccount; final StreamController _streamController; - final Map _openedAccountRecords = {}; ////////////////////////////////////////////////////////////// /// Singleton initialization @@ -60,11 +59,10 @@ class AccountRepository { await _localAccounts.get(); await _userLogins.get(); await _activeLocalAccount.get(); - await _openLoggedInDHTRecords(); } Future close() async { - await _closeLoggedInDHTRecords(); + // ??? } ////////////////////////////////////////////////////////////// @@ -139,25 +137,12 @@ class AccountRepository { activeAccountInfo: null); } - // Pull the account DHT key, decode it and return it - final accountRecord = - _getAccountRecord(userLogin.accountRecordInfo.accountRecord.recordKey); - if (accountRecord == null) { - // Account could not be read or decrypted from DHT - return AccountInfo( - status: AccountInfoStatus.accountInvalid, - active: active, - activeAccountInfo: null); - } - // Got account, decrypted and decoded return AccountInfo( status: AccountInfoStatus.accountReady, active: active, - activeAccountInfo: ActiveAccountInfo( - localAccount: localAccount, - userLogin: userLogin, - accountRecord: accountRecord), + activeAccountInfo: + ActiveAccountInfo(localAccount: localAccount, userLogin: userLogin), ); } @@ -344,9 +329,6 @@ class AccountRepository { await _userLogins.set(newUserLogins); await _activeLocalAccount.set(identityMaster.masterRecordKey); - // Ensure all logins are opened - await _openLoggedInDHTRecords(); - _streamController ..add(AccountRepositoryChange.userLogins) ..add(AccountRepositoryChange.activeLocalAccount); @@ -395,12 +377,6 @@ class AccountRepository { return; } - // Close DHT records for this account - final accountRecordKey = - logoutUserLogin.accountRecordInfo.accountRecord.recordKey; - final accountRecord = _openedAccountRecords.remove(accountRecordKey); - await accountRecord?.close(); - // Remove user from active logins list final newUserLogins = (await _userLogins.get()) .removeWhere((ul) => ul.accountMasterRecordKey == logoutUser); @@ -408,32 +384,7 @@ class AccountRepository { _streamController.add(AccountRepositoryChange.userLogins); } - Future _openLoggedInDHTRecords() async { - // For all user logins if they arent open yet - final userLogins = await _userLogins.get(); - for (final userLogin in userLogins) { - await _openAccountRecord(userLogin); - } - } - - Future _closeLoggedInDHTRecords() async { - final userLogins = await _userLogins.get(); - for (final userLogin in userLogins) { - //// Account record key ///////////////////////////// - final accountRecordKey = - userLogin.accountRecordInfo.accountRecord.recordKey; - await _closeAccountRecord(accountRecordKey); - } - } - - Future _openAccountRecord(UserLogin userLogin) async { - final accountRecordKey = - userLogin.accountRecordInfo.accountRecord.recordKey; - - final existingAccountRecord = _openedAccountRecords[accountRecordKey]; - if (existingAccountRecord != null) { - return existingAccountRecord; - } + Future openAccountRecord(UserLogin userLogin) async { final localAccount = fetchLocalAccount(userLogin.accountMasterRecordKey)!; // Record not yet open, do it @@ -442,19 +393,6 @@ class AccountRepository { userLogin.accountRecordInfo.accountRecord, parent: localAccount.identityMaster.identityRecordKey); - _openedAccountRecords[accountRecordKey] = record; - - // Watch the record's only (default) key - await record.watch(); - return record; } - - DHTRecord? _getAccountRecord(TypedKey accountRecordKey) => - _openedAccountRecords[accountRecordKey]; - - Future _closeAccountRecord(TypedKey accountRecordKey) async { - final accountRecord = _openedAccountRecords.remove(accountRecordKey); - await accountRecord?.close(); - } } diff --git a/lib/chat/cubits/single_contact_messages_cubit.dart b/lib/chat/cubits/single_contact_messages_cubit.dart index cae7b80..4427b89 100644 --- a/lib/chat/cubits/single_contact_messages_cubit.dart +++ b/lib/chat/cubits/single_contact_messages_cubit.dart @@ -154,7 +154,7 @@ class SingleContactMessagesCubit extends Cubit { } Future _mergeMessagesInner( - {required DHTShortArray reconciledMessages, + {required DHTShortArrayWrite reconciledMessagesWriter, required IList messages}) async { // Ensure remoteMessages is sorted by timestamp final newMessages = messages @@ -162,8 +162,12 @@ class SingleContactMessagesCubit extends Cubit { .removeDuplicates(); // Existing messages will always be sorted by timestamp so merging is easy - final existingMessages = - _reconciledChatMessagesCubit!.state.state.data!.value.toList(); + final existingMessages = await reconciledMessagesWriter + .getAllItemsProtobuf(proto.Message.fromBuffer); + if (existingMessages == null) { + throw Exception( + 'Could not load existing reconciled messages at this time'); + } var ePos = 0; var nPos = 0; @@ -180,7 +184,7 @@ class SingleContactMessagesCubit extends Cubit { // New message belongs here // Insert into dht backing array - await reconciledMessages.tryInsertItem( + await reconciledMessagesWriter.tryInsertItem( ePos, newMessage.writeToBuffer()); // Insert into local copy as well for this operation existingMessages.insert(ePos, newMessage); @@ -202,7 +206,7 @@ class SingleContactMessagesCubit extends Cubit { final newMessage = newMessages[nPos]; // Append to dht backing array - await reconciledMessages.tryAddItem(newMessage.writeToBuffer()); + await reconciledMessagesWriter.tryAddItem(newMessage.writeToBuffer()); // Insert into local copy as well for this operation existingMessages.add(newMessage); @@ -215,16 +219,17 @@ class SingleContactMessagesCubit extends Cubit { final reconciledChatMessagesCubit = _reconciledChatMessagesCubit!; // Merge remote and local messages into the reconciled chat log - await reconciledChatMessagesCubit.operate((reconciledMessages) async { + await reconciledChatMessagesCubit + .operateWrite((reconciledMessagesWriter) async { // xxx for now, keep two lists, but can probable simplify this out soon if (entry.localMessages != null) { await _mergeMessagesInner( - reconciledMessages: reconciledMessages, + reconciledMessagesWriter: reconciledMessagesWriter, messages: entry.localMessages!); } if (entry.remoteMessages != null) { await _mergeMessagesInner( - reconciledMessages: reconciledMessages, + reconciledMessagesWriter: reconciledMessagesWriter, messages: entry.remoteMessages!); } }); @@ -244,8 +249,8 @@ class SingleContactMessagesCubit extends Cubit { } Future addMessage({required proto.Message message}) async { - await _localMessagesCubit!.operate( - (shortArray) => shortArray.tryAddItem(message.writeToBuffer())); + await _localMessagesCubit! + .operateWrite((writer) => writer.tryAddItem(message.writeToBuffer())); } final ActiveAccountInfo _activeAccountInfo; diff --git a/lib/chat_list/cubits/chat_list_cubit.dart b/lib/chat_list/cubits/chat_list_cubit.dart index 0591b7e..ea6261b 100644 --- a/lib/chat_list/cubits/chat_list_cubit.dart +++ b/lib/chat_list/cubits/chat_list_cubit.dart @@ -41,13 +41,13 @@ class ChatListCubit extends DHTShortArrayCubit { }) async { // Add Chat to account's list // if this fails, don't keep retrying, user can try again later - await operate((shortArray) async { + await operateWrite((writer) async { final remoteConversationRecordKeyProto = remoteConversationRecordKey.toProto(); // See if we have added this chat already - for (var i = 0; i < shortArray.length; i++) { - final cbuf = await shortArray.getItem(i); + for (var i = 0; i < writer.length; i++) { + final cbuf = await writer.getItem(i); if (cbuf == null) { throw Exception('Failed to get chat'); } @@ -72,7 +72,7 @@ class ChatListCubit extends DHTShortArrayCubit { ..reconciledChatRecord = reconciledChatRecord.toProto(); // Add chat - final added = await shortArray.tryAddItem(chat.writeToBuffer()); + final added = await writer.tryAddItem(chat.writeToBuffer()); if (!added) { throw Exception('Failed to add chat'); } @@ -86,19 +86,19 @@ class ChatListCubit extends DHTShortArrayCubit { // Remove Chat from account's list // if this fails, don't keep retrying, user can try again later - final deletedItem = await operate((shortArray) async { + final (deletedItem, success) = await operateWrite((writer) async { if (activeChatCubit.state == remoteConversationRecordKey) { activeChatCubit.setActiveChat(null); } - for (var i = 0; i < shortArray.length; i++) { - final cbuf = await shortArray.getItem(i); + for (var i = 0; i < writer.length; i++) { + final cbuf = await writer.getItem(i); if (cbuf == null) { throw Exception('Failed to get chat'); } final c = proto.Chat.fromBuffer(cbuf); if (c.remoteConversationRecordKey == remoteConversationKey) { // Found the right chat - if (await shortArray.tryRemoveItem(i) != null) { + if (await writer.tryRemoveItem(i) != null) { return c; } return null; @@ -106,7 +106,7 @@ class ChatListCubit extends DHTShortArrayCubit { } return null; }); - if (deletedItem != null) { + if (success && deletedItem != null) { try { await DHTRecordPool.instance .delete(deletedItem.reconciledChatRecord.toVeilid().recordKey); diff --git a/lib/contact_invitation/cubits/contact_invitation_list_cubit.dart b/lib/contact_invitation/cubits/contact_invitation_list_cubit.dart index 37aafb9..1c3f148 100644 --- a/lib/contact_invitation/cubits/contact_invitation_list_cubit.dart +++ b/lib/contact_invitation/cubits/contact_invitation_list_cubit.dart @@ -139,8 +139,8 @@ class ContactInvitationListCubit // Add ContactInvitationRecord to account's list // if this fails, don't keep retrying, user can try again later - await operate((shortArray) async { - if (await shortArray.tryAddItem(cinvrec.writeToBuffer()) == false) { + await operateWrite((writer) async { + if (await writer.tryAddItem(cinvrec.writeToBuffer()) == false) { throw Exception('Failed to add contact invitation record'); } }); @@ -158,16 +158,16 @@ class ContactInvitationListCubit _activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey; // Remove ContactInvitationRecord from account's list - final deletedItem = await operate((shortArray) async { - for (var i = 0; i < shortArray.length; i++) { - final item = await shortArray.getItemProtobuf( + final (deletedItem, success) = await operateWrite((writer) async { + for (var i = 0; i < writer.length; i++) { + final item = await writer.getItemProtobuf( proto.ContactInvitationRecord.fromBuffer, i); if (item == null) { throw Exception('Failed to get contact invitation record'); } if (item.contactRequestInbox.recordKey.toVeilid() == contactRequestInboxRecordKey) { - if (await shortArray.tryRemoveItem(i) != null) { + if (await writer.tryRemoveItem(i) != null) { return item; } return null; @@ -176,7 +176,7 @@ class ContactInvitationListCubit return null; }); - if (deletedItem != null) { + if (success && deletedItem != null) { // Delete the contact request inbox final contactRequestInbox = deletedItem.contactRequestInbox.toVeilid(); await (await pool.openOwned(contactRequestInbox, diff --git a/lib/contact_invitation/cubits/contact_request_inbox_cubit.dart b/lib/contact_invitation/cubits/contact_request_inbox_cubit.dart index 80fa6e7..80c18ae 100644 --- a/lib/contact_invitation/cubits/contact_request_inbox_cubit.dart +++ b/lib/contact_invitation/cubits/contact_request_inbox_cubit.dart @@ -14,11 +14,11 @@ class ContactRequestInboxCubit contactInvitationRecord: contactInvitationRecord), decodeState: proto.SignedContactResponse.fromBuffer); - ContactRequestInboxCubit.value( - {required super.record, - required this.activeAccountInfo, - required this.contactInvitationRecord}) - : super.value(decodeState: proto.SignedContactResponse.fromBuffer); + // ContactRequestInboxCubit.value( + // {required super.record, + // required this.activeAccountInfo, + // required this.contactInvitationRecord}) + // : super.value(decodeState: proto.SignedContactResponse.fromBuffer); static Future _open( {required ActiveAccountInfo activeAccountInfo, diff --git a/lib/contact_invitation/cubits/waiting_invitations_bloc_map_cubit.dart b/lib/contact_invitation/cubits/waiting_invitations_bloc_map_cubit.dart index 584d2a1..c674a14 100644 --- a/lib/contact_invitation/cubits/waiting_invitations_bloc_map_cubit.dart +++ b/lib/contact_invitation/cubits/waiting_invitations_bloc_map_cubit.dart @@ -23,7 +23,7 @@ class WaitingInvitationsBlocMapCubit extends BlocMapCubit addWaitingInvitation( + Future _addWaitingInvitation( {required proto.ContactInvitationRecord contactInvitationRecord}) async => add(() => MapEntry( @@ -54,7 +54,7 @@ class WaitingInvitationsBlocMapCubit extends BlocMapCubit updateState(TypedKey key, proto.ContactInvitationRecord value) => - addWaitingInvitation(contactInvitationRecord: value); + _addWaitingInvitation(contactInvitationRecord: value); //// final ActiveAccountInfo activeAccountInfo; diff --git a/lib/contacts/cubits/contact_list_cubit.dart b/lib/contacts/cubits/contact_list_cubit.dart index 7f23475..99f13bf 100644 --- a/lib/contacts/cubits/contact_list_cubit.dart +++ b/lib/contacts/cubits/contact_list_cubit.dart @@ -52,8 +52,8 @@ class ContactListCubit extends DHTShortArrayCubit { // Add Contact to account's list // if this fails, don't keep retrying, user can try again later - await operate((shortArray) async { - if (await shortArray.tryAddItem(contact.writeToBuffer()) == false) { + await operateWrite((writer) async { + if (!await writer.tryAddItem(contact.writeToBuffer())) { throw Exception('Failed to add contact record'); } }); @@ -66,16 +66,15 @@ class ContactListCubit extends DHTShortArrayCubit { contact.remoteConversationRecordKey.toVeilid(); // Remove Contact from account's list - final deletedItem = await operate((shortArray) async { - for (var i = 0; i < shortArray.length; i++) { - final item = - await shortArray.getItemProtobuf(proto.Contact.fromBuffer, i); + final (deletedItem, success) = await operateWrite((writer) async { + for (var i = 0; i < writer.length; i++) { + final item = await writer.getItemProtobuf(proto.Contact.fromBuffer, i); if (item == null) { throw Exception('Failed to get contact'); } if (item.remoteConversationRecordKey == contact.remoteConversationRecordKey) { - if (await shortArray.tryRemoveItem(i) != null) { + if (await writer.tryRemoveItem(i) != null) { return item; } return null; @@ -84,7 +83,7 @@ class ContactListCubit extends DHTShortArrayCubit { return null; }); - if (deletedItem != null) { + if (success && deletedItem != null) { try { await pool.delete(localConversationKey); } on Exception catch (e) { diff --git a/lib/contacts/cubits/conversation_cubit.dart b/lib/contacts/cubits/conversation_cubit.dart index 23c61c6..34c609d 100644 --- a/lib/contacts/cubits/conversation_cubit.dart +++ b/lib/contacts/cubits/conversation_cubit.dart @@ -41,31 +41,35 @@ class ConversationCubit extends Cubit> { super(const AsyncValue.loading()) { if (_localConversationRecordKey != null) { Future.delayed(Duration.zero, () async { - final accountRecordKey = _activeAccountInfo - .userLogin.accountRecordInfo.accountRecord.recordKey; + await _setLocalConversation(() async { + final accountRecordKey = _activeAccountInfo + .userLogin.accountRecordInfo.accountRecord.recordKey; - // Open local record key if it is specified - final pool = DHTRecordPool.instance; - final crypto = await _cachedConversationCrypto(); - final writer = _activeAccountInfo.conversationWriter; - final record = await pool.openWrite( - _localConversationRecordKey!, writer, - parent: accountRecordKey, crypto: crypto); - await _setLocalConversation(record); + // Open local record key if it is specified + final pool = DHTRecordPool.instance; + final crypto = await _cachedConversationCrypto(); + final writer = _activeAccountInfo.conversationWriter; + final record = await pool.openWrite( + _localConversationRecordKey!, writer, + parent: accountRecordKey, crypto: crypto); + return record; + }); }); } if (_remoteConversationRecordKey != null) { Future.delayed(Duration.zero, () async { - final accountRecordKey = _activeAccountInfo - .userLogin.accountRecordInfo.accountRecord.recordKey; + await _setRemoteConversation(() async { + final accountRecordKey = _activeAccountInfo + .userLogin.accountRecordInfo.accountRecord.recordKey; - // Open remote record key if it is specified - final pool = DHTRecordPool.instance; - final crypto = await _cachedConversationCrypto(); - final record = await pool.openRead(_remoteConversationRecordKey, - parent: accountRecordKey, crypto: crypto); - await _setRemoteConversation(record); + // Open remote record key if it is specified + final pool = DHTRecordPool.instance; + final crypto = await _cachedConversationCrypto(); + final record = await pool.openRead(_remoteConversationRecordKey, + parent: accountRecordKey, crypto: crypto); + return record; + }); }); } } @@ -74,6 +78,9 @@ class ConversationCubit extends Cubit> { Future close() async { await _localSubscription?.cancel(); await _remoteSubscription?.cancel(); + await _localConversationCubit?.close(); + await _remoteConversationCubit?.close(); + await super.close(); } @@ -122,24 +129,21 @@ class ConversationCubit extends Cubit> { } // Open local converation key - Future _setLocalConversation(DHTRecord localConversationRecord) async { + Future _setLocalConversation(Future Function() open) async { assert(_localConversationCubit == null, 'shoud not set local conversation twice'); - _localConversationCubit = DefaultDHTRecordCubit.value( - record: localConversationRecord, - decodeState: proto.Conversation.fromBuffer); + _localConversationCubit = DefaultDHTRecordCubit( + open: open, decodeState: proto.Conversation.fromBuffer); _localSubscription = _localConversationCubit!.stream.listen(updateLocalConversationState); } // Open remote converation key - Future _setRemoteConversation( - DHTRecord remoteConversationRecord) async { + Future _setRemoteConversation(Future Function() open) async { assert(_remoteConversationCubit == null, 'shoud not set remote conversation twice'); - _remoteConversationCubit = DefaultDHTRecordCubit.value( - record: remoteConversationRecord, - decodeState: proto.Conversation.fromBuffer); + _remoteConversationCubit = DefaultDHTRecordCubit( + open: open, decodeState: proto.Conversation.fromBuffer); _remoteSubscription = _remoteConversationCubit!.stream.listen(updateRemoteConversationState); } @@ -215,7 +219,7 @@ class ConversationCubit extends Cubit> { // If success, save the new local conversation record key in this object _localConversationRecordKey = localConversationRecord.key; - await _setLocalConversation(localConversationRecord); + await _setLocalConversation(() async => localConversationRecord); return out; } diff --git a/lib/layout/home/home_account_ready/home_account_ready_shell.dart b/lib/layout/home/home_account_ready/home_account_ready_shell.dart index 1ccf562..2cdce7e 100644 --- a/lib/layout/home/home_account_ready/home_account_ready_shell.dart +++ b/lib/layout/home/home_account_ready/home_account_ready_shell.dart @@ -19,14 +19,11 @@ class HomeAccountReadyShell extends StatefulWidget { // These must exist in order for the account to // be considered 'ready' for this widget subtree final activeLocalAccount = context.read().state!; - final accountInfo = - AccountRepository.instance.getAccountInfo(activeLocalAccount); - final activeAccountInfo = accountInfo.activeAccountInfo!; + final activeAccountInfo = context.read(); final routerCubit = context.read(); return HomeAccountReadyShell._( activeLocalAccount: activeLocalAccount, - accountInfo: accountInfo, activeAccountInfo: activeAccountInfo, routerCubit: routerCubit, key: key, @@ -34,7 +31,6 @@ class HomeAccountReadyShell extends StatefulWidget { } const HomeAccountReadyShell._( {required this.activeLocalAccount, - required this.accountInfo, required this.activeAccountInfo, required this.routerCubit, required this.child, @@ -45,7 +41,6 @@ class HomeAccountReadyShell extends StatefulWidget { final Widget child; final TypedKey activeLocalAccount; - final AccountInfo accountInfo; final ActiveAccountInfo activeAccountInfo; final RouterCubit routerCubit; @@ -55,7 +50,6 @@ class HomeAccountReadyShell extends StatefulWidget { properties ..add(DiagnosticsProperty( 'activeLocalAccount', activeLocalAccount)) - ..add(DiagnosticsProperty('accountInfo', accountInfo)) ..add(DiagnosticsProperty( 'activeAccountInfo', activeAccountInfo)) ..add(DiagnosticsProperty('routerCubit', routerCubit)); @@ -114,61 +108,52 @@ class HomeAccountReadyShellState extends State { } @override - Widget build(BuildContext context) => Provider.value( - value: widget.activeAccountInfo, - child: BlocProvider( - create: (context) => AccountRecordCubit( - record: widget.activeAccountInfo.accountRecord), - child: Builder(builder: (context) { - final account = - context.watch().state.data?.value; - if (account == null) { - return waitingPage(); - } - return MultiBlocProvider( - providers: [ - BlocProvider( - create: (context) => ContactInvitationListCubit( - activeAccountInfo: widget.activeAccountInfo, - account: account)), - BlocProvider( - create: (context) => ContactListCubit( - activeAccountInfo: widget.activeAccountInfo, - account: account)), - BlocProvider( - create: (context) => ActiveChatCubit(null) - ..withStateListen((event) { - widget.routerCubit.setHasActiveChat(event != null); - })), - BlocProvider( - create: (context) => ChatListCubit( - activeAccountInfo: widget.activeAccountInfo, - activeChatCubit: context.read(), - account: account)), - BlocProvider( - create: (context) => ActiveConversationsBlocMapCubit( - activeAccountInfo: widget.activeAccountInfo, - contactListCubit: context.read()) - ..followBloc(context.read())), - BlocProvider( - create: (context) => ActiveSingleContactChatBlocMapCubit( - activeAccountInfo: widget.activeAccountInfo, - contactListCubit: context.read(), - chatListCubit: context.read()) - ..followBloc( - context.read())), - BlocProvider( - create: (context) => WaitingInvitationsBlocMapCubit( - activeAccountInfo: widget.activeAccountInfo, - account: account) - ..followBloc( - context.read())) - ], - child: MultiBlocListener(listeners: [ - BlocListener( - listener: _invitationStatusListener, - ) - ], child: widget.child)); - }))); + Widget build(BuildContext context) { + final account = context.watch().state.data?.value; + if (account == null) { + return waitingPage(); + } + return MultiBlocProvider( + providers: [ + BlocProvider( + create: (context) => ContactInvitationListCubit( + activeAccountInfo: widget.activeAccountInfo, + account: account)), + BlocProvider( + create: (context) => ContactListCubit( + activeAccountInfo: widget.activeAccountInfo, + account: account)), + BlocProvider( + create: (context) => ActiveChatCubit(null) + ..withStateListen((event) { + widget.routerCubit.setHasActiveChat(event != null); + })), + BlocProvider( + create: (context) => ChatListCubit( + activeAccountInfo: widget.activeAccountInfo, + activeChatCubit: context.read(), + account: account)), + BlocProvider( + create: (context) => ActiveConversationsBlocMapCubit( + activeAccountInfo: widget.activeAccountInfo, + contactListCubit: context.read()) + ..followBloc(context.read())), + BlocProvider( + create: (context) => ActiveSingleContactChatBlocMapCubit( + activeAccountInfo: widget.activeAccountInfo, + contactListCubit: context.read(), + chatListCubit: context.read()) + ..followBloc(context.read())), + BlocProvider( + create: (context) => WaitingInvitationsBlocMapCubit( + activeAccountInfo: widget.activeAccountInfo, account: account) + ..followBloc(context.read())) + ], + child: MultiBlocListener(listeners: [ + BlocListener( + listener: _invitationStatusListener, + ) + ], child: widget.child)); + } } diff --git a/lib/layout/home/home_shell.dart b/lib/layout/home/home_shell.dart index 839a8a3..bd1949c 100644 --- a/lib/layout/home/home_shell.dart +++ b/lib/layout/home/home_shell.dart @@ -55,7 +55,9 @@ class HomeShellState extends State { value: accountInfo.activeAccountInfo!, child: BlocProvider( create: (context) => AccountRecordCubit( - record: accountInfo.activeAccountInfo!.accountRecord), + open: () async => AccountRepository.instance + .openAccountRecord( + accountInfo.activeAccountInfo!.userLogin)), child: widget.accountReadyBuilder)); } } diff --git a/macos/Podfile.lock b/macos/Podfile.lock index 88749a1..7ca005d 100644 --- a/macos/Podfile.lock +++ b/macos/Podfile.lock @@ -72,7 +72,7 @@ SPEC CHECKSUMS: pasteboard: 9b69dba6fedbb04866be632205d532fe2f6b1d99 path_provider_foundation: 3784922295ac71e43754bd15e0653ccfd36a147c screen_retriever: 59634572a57080243dd1bf715e55b6c54f241a38 - share_plus: 76dd39142738f7a68dd57b05093b5e8193f220f7 + share_plus: 36537c04ce0c3e3f5bd297ce4318b6d5ee5fd6cf shared_preferences_foundation: b4c3b4cddf1c21f02770737f147a3f5da9d39695 smart_auth: b38e3ab4bfe089eacb1e233aca1a2340f96c28e9 sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec diff --git a/packages/async_tools/lib/src/single_stateless_processor.dart b/packages/async_tools/lib/src/single_stateless_processor.dart index 1b96b74..7cb7ff0 100644 --- a/packages/async_tools/lib/src/single_stateless_processor.dart +++ b/packages/async_tools/lib/src/single_stateless_processor.dart @@ -24,7 +24,8 @@ class SingleStatelessProcessor { }); } - // Like update, but with a busy wrapper that clears once the updating is finished + // Like update, but with a busy wrapper that + // clears once the updating is finished void busyUpdate( Future Function(Future Function(void Function(S))) busy, Future Function(void Function(S)) closure) { diff --git a/packages/bloc_tools/lib/src/async_transformer_cubit.dart b/packages/bloc_tools/lib/src/async_transformer_cubit.dart index d32f37e..fa6eacb 100644 --- a/packages/bloc_tools/lib/src/async_transformer_cubit.dart +++ b/packages/bloc_tools/lib/src/async_transformer_cubit.dart @@ -3,6 +3,11 @@ import 'dart:async'; import 'package:async_tools/async_tools.dart'; import 'package:bloc/bloc.dart'; +// A cubit with state T that wraps another input cubit of state S and +// produces T fro S via an asynchronous transform closure +// The input cubit becomes 'owned' by the AsyncTransformerCubit and will +// be closed when the AsyncTransformerCubit closes. + class AsyncTransformerCubit extends Cubit> { AsyncTransformerCubit(this.input, {required this.transform}) : super(const AsyncValue.loading()) { diff --git a/packages/veilid_support/lib/dht_support/src/dht_record/dht_record.dart b/packages/veilid_support/lib/dht_support/src/dht_record/dht_record.dart index b5ae275..fa98b9d 100644 --- a/packages/veilid_support/lib/dht_support/src/dht_record/dht_record.dart +++ b/packages/veilid_support/lib/dht_support/src/dht_record/dht_record.dart @@ -342,7 +342,7 @@ class DHTRecord { void _addValueChange( {required bool local, - required Uint8List data, + required Uint8List? data, required List subkeys}) { final ws = watchState; if (ws != null) { @@ -378,6 +378,6 @@ class DHTRecord { void _addRemoteValueChange(VeilidUpdateValueChange update) { _addValueChange( - local: false, data: update.value.data, subkeys: update.subkeys); + local: false, data: update.value?.data, subkeys: update.subkeys); } } diff --git a/packages/veilid_support/lib/dht_support/src/dht_record/dht_record_cubit.dart b/packages/veilid_support/lib/dht_support/src/dht_record/dht_record_cubit.dart index 2295d19..647c431 100644 --- a/packages/veilid_support/lib/dht_support/src/dht_record/dht_record_cubit.dart +++ b/packages/veilid_support/lib/dht_support/src/dht_record/dht_record_cubit.dart @@ -28,19 +28,19 @@ class DHTRecordCubit extends Cubit> { }); } - DHTRecordCubit.value({ - required DHTRecord record, - required InitialStateFunction initialStateFunction, - required StateFunction stateFunction, - required WatchFunction watchFunction, - }) : _record = record, - _stateFunction = stateFunction, - _wantsCloseRecord = false, - super(const AsyncValue.loading()) { - Future.delayed(Duration.zero, () async { - await _init(initialStateFunction, stateFunction, watchFunction); - }); - } + // DHTRecordCubit.value({ + // required DHTRecord record, + // required InitialStateFunction initialStateFunction, + // required StateFunction stateFunction, + // required WatchFunction watchFunction, + // }) : _record = record, + // _stateFunction = stateFunction, + // _wantsCloseRecord = false, + // super(const AsyncValue.loading()) { + // Future.delayed(Duration.zero, () async { + // await _init(initialStateFunction, stateFunction, watchFunction); + // }); + // } Future _init( InitialStateFunction initialStateFunction, @@ -123,13 +123,13 @@ class DefaultDHTRecordCubit extends DHTRecordCubit { stateFunction: _makeStateFunction(decodeState), watchFunction: _makeWatchFunction()); - DefaultDHTRecordCubit.value({ - required super.record, - required T Function(List data) decodeState, - }) : super.value( - initialStateFunction: _makeInitialStateFunction(decodeState), - stateFunction: _makeStateFunction(decodeState), - watchFunction: _makeWatchFunction()); + // DefaultDHTRecordCubit.value({ + // required super.record, + // required T Function(List data) decodeState, + // }) : super.value( + // initialStateFunction: _makeInitialStateFunction(decodeState), + // stateFunction: _makeStateFunction(decodeState), + // watchFunction: _makeWatchFunction()); static InitialStateFunction _makeInitialStateFunction( T Function(List data) decodeState) => diff --git a/packages/veilid_support/lib/dht_support/src/dht_short_array/dht_short_array.dart b/packages/veilid_support/lib/dht_support/src/dht_short_array/dht_short_array.dart index fe8b705..5b115ae 100644 --- a/packages/veilid_support/lib/dht_support/src/dht_short_array/dht_short_array.dart +++ b/packages/veilid_support/lib/dht_support/src/dht_short_array/dht_short_array.dart @@ -1,7 +1,6 @@ import 'dart:async'; import 'dart:typed_data'; -import 'package:async_tools/async_tools.dart'; import 'package:mutex/mutex.dart'; import 'package:protobuf/protobuf.dart'; @@ -169,90 +168,36 @@ class DHTShortArray { } /// Runs a closure allowing read-only access to the shortarray - Future operate(Future Function(DHTShortArrayRead) closure) async => + Future operate(Future Function(DHTShortArrayRead) closure) async => _head.operate((head) async { final reader = _DHTShortArrayRead._(head); return closure(reader); }); /// Runs a closure allowing read-write access to the shortarray + /// Makes only one attempt to consistently write the changes to the DHT /// Returns (result, true) of the closure if the write could be performed /// Returns (null, false) if the write could not be performed at this time Future<(T?, bool)> operateWrite( - Future Function(DHTShortArrayWrite) closure) async => + Future Function(DHTShortArrayWrite) closure) async => _head.operateWrite((head) async { final writer = _DHTShortArrayWrite._(head); return closure(writer); }); - /// Set an item at position 'pos' of the DHTShortArray. Retries until the - /// value being written is successfully made the newest value of the element. - /// This may throw an exception if the position elements the built-in limit of - /// 'maxElements = 256' entries. - Future eventualWriteItem(int pos, Uint8List newValue, - {Duration? timeout}) async { - await _head.operateWriteEventual((head) async { - bool wasSet; - (_, wasSet) = await _tryWriteItemInner(head, pos, newValue); - return wasSet; - }, timeout: timeout); - } - - /// Change an item at position 'pos' of the DHTShortArray. - /// Runs with the value of the old element at that position such that it can - /// be changed to the returned value from tha closure. Retries until the - /// value being written is successfully made the newest value of the element. - /// This may throw an exception if the position elements the built-in limit of - /// 'maxElements = 256' entries. - - Future eventualUpdateItem( - int pos, Future Function(Uint8List? oldValue) update, - {Duration? timeout}) async { - await _head.operateWriteEventual((head) async { - final oldData = await getItem(pos); - - // Update the data - final updatedData = await update(oldData); - - // Set it back - bool wasSet; - (_, wasSet) = await _tryWriteItemInner(head, pos, updatedData); - return wasSet; - }, timeout: timeout); - } - - /// Convenience function: - /// Like eventualWriteItem but also encodes the input value as JSON and parses - /// the returned element as JSON - Future eventualWriteItemJson(int pos, T newValue, - {Duration? timeout}) => - eventualWriteItem(pos, jsonEncodeBytes(newValue), timeout: timeout); - - /// Convenience function: - /// Like eventualWriteItem but also encodes the input value as a protobuf - /// object and parses the returned element as a protobuf object - Future eventualWriteItemProtobuf( - int pos, T newValue, - {int subkey = -1, Duration? timeout}) => - eventualWriteItem(pos, newValue.writeToBuffer(), timeout: timeout); - - /// Convenience function: - /// Like eventualUpdateItem but also encodes the input value as JSON - Future eventualUpdateItemJson( - T Function(dynamic) fromJson, int pos, Future Function(T?) update, - {Duration? timeout}) => - eventualUpdateItem(pos, jsonUpdate(fromJson, update), timeout: timeout); - - /// Convenience function: - /// Like eventualUpdateItem but also encodes the input value as a protobuf - /// object - Future eventualUpdateItemProtobuf( - T Function(List) fromBuffer, - int pos, - Future Function(T?) update, - {Duration? timeout}) => - eventualUpdateItem(pos, protobufUpdate(fromBuffer, update), - timeout: timeout); + /// Runs a closure allowing read-write access to the shortarray + /// Will execute the closure multiple times if a consistent write to the DHT + /// is not achieved. Timeout if specified will be thrown as a + /// TimeoutException. The closure should return true if its changes also + /// succeeded, returning false will trigger another eventual consistency + /// attempt. + Future operateWriteEventual( + Future Function(DHTShortArrayWrite) closure, + {Duration? timeout}) async => + _head.operateWriteEventual((head) async { + final writer = _DHTShortArrayWrite._(head); + return closure(writer); + }, timeout: timeout); Future> listen( void Function() onChanged, diff --git a/packages/veilid_support/lib/dht_support/src/dht_short_array/dht_short_array_cubit.dart b/packages/veilid_support/lib/dht_support/src/dht_short_array/dht_short_array_cubit.dart index 5eea23a..c145a67 100644 --- a/packages/veilid_support/lib/dht_support/src/dht_short_array/dht_short_array_cubit.dart +++ b/packages/veilid_support/lib/dht_support/src/dht_short_array/dht_short_array_cubit.dart @@ -4,7 +4,6 @@ import 'package:async_tools/async_tools.dart'; import 'package:bloc/bloc.dart'; import 'package:bloc_tools/bloc_tools.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; -import 'package:mutex/mutex.dart'; import '../../../veilid_support.dart'; @@ -29,18 +28,18 @@ class DHTShortArrayCubit extends Cubit> }); } - DHTShortArrayCubit.value({ - required DHTShortArray shortArray, - required T Function(List data) decodeElement, - }) : _shortArray = shortArray, - _decodeElement = decodeElement, - super(const BlocBusyState(AsyncValue.loading())) { - _initFuture = Future(() async { - // Make initial state update - unawaited(_refreshNoWait()); - _subscription = await shortArray.listen(_update); - }); - } + // DHTShortArrayCubit.value({ + // required DHTShortArray shortArray, + // required T Function(List data) decodeElement, + // }) : _shortArray = shortArray, + // _decodeElement = decodeElement, + // super(const BlocBusyState(AsyncValue.loading())) { + // _initFuture = Future(() async { + // // Make initial state update + // unawaited(_refreshNoWait()); + // _subscription = await shortArray.listen(_update); + // }); + // } Future refresh({bool forceRefresh = false}) async { await _initFuture; @@ -48,16 +47,15 @@ class DHTShortArrayCubit extends Cubit> } Future _refreshNoWait({bool forceRefresh = false}) async => - busy((emit) async => _operateMutex.protect( - () async => _refreshInner(emit, forceRefresh: forceRefresh))); + busy((emit) async => _refreshInner(emit, forceRefresh: forceRefresh)); Future _refreshInner(void Function(AsyncValue>) emit, {bool forceRefresh = false}) async { try { - final newState = - (await _shortArray.getAllItems(forceRefresh: forceRefresh)) - ?.map(_decodeElement) - .toIList(); + final newState = (await _shortArray.operate( + (reader) => reader.getAllItems(forceRefresh: forceRefresh))) + ?.map(_decodeElement) + .toIList(); if (newState != null) { emit(AsyncValue.data(newState)); } @@ -71,8 +69,8 @@ class DHTShortArrayCubit extends Cubit> // Because this is async, we could get an update while we're // still processing the last one. Only called after init future has run // so we dont have to wait for that here. - _sspUpdate.busyUpdate>>(busy, - (emit) async => _operateMutex.protect(() async => _refreshInner(emit))); + _sspUpdate.busyUpdate>>( + busy, (emit) async => _refreshInner(emit)); } @override @@ -86,12 +84,24 @@ class DHTShortArrayCubit extends Cubit> await super.close(); } - Future operate(Future Function(DHTShortArray) closure) async { + Future operate(Future Function(DHTShortArrayRead) closure) async { await _initFuture; - return _operateMutex.protect(() async => closure(_shortArray)); + return _shortArray.operate(closure); + } + + Future<(R?, bool)> operateWrite( + Future Function(DHTShortArrayWrite) closure) async { + await _initFuture; + return _shortArray.operateWrite(closure); + } + + Future operateWriteEventual( + Future Function(DHTShortArrayWrite) closure, + {Duration? timeout}) async { + await _initFuture; + return _shortArray.operateWriteEventual(closure, timeout: timeout); } - final _operateMutex = Mutex(); late final Future _initFuture; late final DHTShortArray _shortArray; final T Function(List data) _decodeElement; diff --git a/packages/veilid_support/lib/dht_support/src/dht_short_array/dht_short_array_head.dart b/packages/veilid_support/lib/dht_support/src/dht_short_array/dht_short_array_head.dart index bd06663..779cbcb 100644 --- a/packages/veilid_support/lib/dht_support/src/dht_short_array/dht_short_array_head.dart +++ b/packages/veilid_support/lib/dht_support/src/dht_short_array/dht_short_array_head.dart @@ -32,7 +32,7 @@ class _DHTShortArrayHead { final head = proto.DHTShortArray(); head.keys.addAll(_linkedRecords.map((lr) => lr.key.toProto())); - head.index.addAll(_index); + head.index = List.of(_index); head.seqs.addAll(_seqs); // Do not serialize free list, it gets recreated // Do not serialize local seqs, they are only locally relevant @@ -58,7 +58,7 @@ class _DHTShortArrayHead { }); Future<(T?, bool)> operateWrite( - Future Function(_DHTShortArrayHead) closure) async => + Future Function(_DHTShortArrayHead) closure) async => _headMutex.protect(() async { final oldLinkedRecords = List.of(_linkedRecords); final oldIndex = List.of(_index); @@ -111,14 +111,22 @@ class _DHTShortArrayHead { oldSeqs = List.of(_seqs); // Try to do the element write - do { + while (true) { if (timeoutTs != null) { final now = Veilid.instance.now(); if (now >= timeoutTs) { throw TimeoutException('timeout reached'); } } - } while (!await closure(this)); + if (await closure(this)) { + break; + } + // Failed to write in closure resets state + _linkedRecords = List.of(oldLinkedRecords); + _index = List.of(oldIndex); + _free = List.of(oldFree); + _seqs = List.of(oldSeqs); + } // Try to do the head write } while (!await _writeHead()); From 8e4cd0dfe066c95ac905092045006872962602a6 Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Mon, 1 Apr 2024 09:12:55 -0400 Subject: [PATCH 68/68] pub upgrade --- packages/veilid_support/pubspec.lock | 20 ++++++-------------- pubspec.lock | 12 ++++++------ pubspec.yaml | 4 ++-- 3 files changed, 14 insertions(+), 22 deletions(-) diff --git a/packages/veilid_support/pubspec.lock b/packages/veilid_support/pubspec.lock index 7e13fbb..f4835ef 100644 --- a/packages/veilid_support/pubspec.lock +++ b/packages/veilid_support/pubspec.lock @@ -131,10 +131,10 @@ packages: dependency: transitive description: name: change_case - sha256: f4e08feaa845e75e4f5ad2b0e15f24813d7ea6c27e7b78252f0c17f752cf1157 + sha256: "47c48c36f95f20c6d0ba03efabceff261d05026cca322cc2c4c01c343371b5bb" url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "2.0.1" characters: dependency: transitive description: @@ -239,14 +239,6 @@ packages: url: "https://pub.dev" source: hosted version: "7.0.0" - file_utils: - dependency: transitive - description: - name: file_utils - sha256: d1e64389a22649095c8405c9e177272caf05139255931c9ff30d53b5c9bcaa34 - url: "https://pub.dev" - source: hosted - version: "1.0.1" fixnum: dependency: transitive description: @@ -657,10 +649,10 @@ packages: dependency: transitive description: name: system_info2 - sha256: af2f948e3f31a3367a049932a8ad59faf0063ecf836a020d975b9f41566d8bc9 + sha256: "65206bbef475217008b5827374767550a5420ce70a04d2d7e94d1d2253f3efc9" url: "https://pub.dev" source: hosted - version: "3.0.2" + version: "4.0.0" system_info_plus: dependency: transitive description: @@ -731,7 +723,7 @@ packages: path: "../../../veilid/veilid-flutter" relative: true source: path - version: "0.2.5" + version: "0.3.0" vm_service: dependency: transitive description: @@ -798,4 +790,4 @@ packages: version: "3.1.2" sdks: dart: ">=3.3.0 <4.0.0" - flutter: ">=3.10.6" + flutter: ">=3.19.1" diff --git a/pubspec.lock b/pubspec.lock index c2cf50b..751e310 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -403,10 +403,10 @@ packages: dependency: "direct main" description: name: fast_immutable_collections - sha256: "49154d1da38a34519b907b0e94a06705a59b7127728131dc4a54fe62fd95a83e" + sha256: "38fbc50df5b219dcfb83ebbc3275ec09872530ca1153858fc56fceadb310d037" url: "https://pub.dev" source: hosted - version: "10.2.1" + version: "10.2.2" ffi: dependency: transitive description: @@ -826,10 +826,10 @@ packages: dependency: "direct main" description: name: motion_toast - sha256: e423584213b459d021fbdfcd8e02b22e3480ff0b3cef05dc4cde040595ebf084 + sha256: f3fe9f92d9956814a1aa040c22c8a6c432cfb0c9f783163d9ec64915838e4837 url: "https://pub.dev" source: hosted - version: "2.8.0" + version: "2.9.0" mutex: dependency: "direct main" description: @@ -1548,10 +1548,10 @@ packages: dependency: transitive description: name: win32 - sha256: "8cb58b45c47dcb42ab3651533626161d6b67a2921917d8d429791f76972b3480" + sha256: "0a989dc7ca2bb51eac91e8fd00851297cfffd641aa7538b165c62637ca0eaa4a" url: "https://pub.dev" source: hosted - version: "5.3.0" + version: "5.4.0" window_manager: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index e03d37b..45644d0 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -27,7 +27,7 @@ dependencies: cool_dropdown: ^2.1.0 cupertino_icons: ^1.0.6 equatable: ^2.0.5 - fast_immutable_collections: ^10.2.1 + fast_immutable_collections: ^10.2.2 fixnum: ^1.1.0 flutter: sdk: flutter @@ -54,7 +54,7 @@ dependencies: loggy: ^2.0.3 meta: ^1.11.0 mobile_scanner: ^4.0.1 - motion_toast: ^2.8.0 + motion_toast: ^2.9.0 mutex: path: packages/mutex pasteboard: ^0.2.0